tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 252
 253        See also: `SearchByTicker()`, `SearchInstruments()`.
 254        """
 255
 256        self.figi = ""
 257        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 258
 259        See also: `SearchByFIGI()`, `SearchInstruments()`.
 260        """
 261
 262        self.depth = 1
 263        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 264
 265        See also: `GetCurrentPrices()`.
 266        """
 267
 268        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 269        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 270
 271        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 272        """
 273
 274        uLogger.debug("Broker API server: {}".format(self.server))
 275
 276        self.timeout = 15
 277        """Server operations timeout in seconds. Default: `15`.
 278
 279        See also: `SendAPIRequest()`.
 280        """
 281
 282        self.headers = {
 283            "Content-Type": "application/json",
 284            "accept": "application/json",
 285            "Authorization": "Bearer {}".format(self.token),
 286            "x-app-name": "Tim55667757.TKSBrokerAPI",
 287        }
 288        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.body = None
 294        """Request body which send to broker server. Default: `None`.
 295
 296        See also: `SendAPIRequest()`.
 297        """
 298
 299        self.moreDebug = False
 300        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 301
 302        self.historyFile = None
 303        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 304
 305        See also: `History()`.
 306        """
 307
 308        self.htmlHistoryFile = "index.html"
 309        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 310
 311        See also: `ShowHistoryChart()`.
 312        """
 313
 314        self.instrumentsFile = "instruments.md"
 315        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 316
 317        See also: `ShowInstrumentsInfo()`.
 318        """
 319
 320        self.searchResultsFile = "search-results.md"
 321        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 322
 323        See also: `SearchInstruments()`.
 324        """
 325
 326        self.pricesFile = "prices.md"
 327        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 328
 329        See also: `GetListOfPrices()`.
 330        """
 331
 332        self.infoFile = "info.md"
 333        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 334
 335        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 336        """
 337
 338        self.bondsXLSXFile = "ext-bonds.xlsx"
 339        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 340        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 341
 342        See also: `ExtendBondsData()`.
 343        """
 344
 345        self.calendarFile = "calendar.md"
 346        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 347        
 348        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 349
 350        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 351        """
 352
 353        self.overviewFile = "overview.md"
 354        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 355
 356        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 357        """
 358
 359        self.overviewDigestFile = "overview-digest.md"
 360        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 361
 362        See also: `Overview()` with parameter `details="digest"`.
 363        """
 364
 365        self.overviewPositionsFile = "overview-positions.md"
 366        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 367
 368        See also: `Overview()` with parameter `details="positions"`.
 369        """
 370
 371        self.overviewOrdersFile = "overview-orders.md"
 372        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 373
 374        See also: `Overview()` with parameter `details="orders"`.
 375        """
 376
 377        self.overviewAnalyticsFile = "overview-analytics.md"
 378        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 379
 380        See also: `Overview()` with parameter `details="analytics"`.
 381        """
 382
 383        self.reportFile = "deals.md"
 384        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 385
 386        See also: `Deals()`.
 387        """
 388
 389        self.withdrawalLimitsFile = "limits.md"
 390        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 391
 392        See also: `OverviewLimits()` and `RequestLimits()`.
 393        """
 394
 395        self.userInfoFile = "user-info.md"
 396        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 397
 398        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 399        """
 400
 401        self.userAccountsFile = "accounts.md"
 402        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 403
 404        See also: `OverviewAccounts()`, `RequestAccounts()`.
 405        """
 406
 407        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 408        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 409
 410        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 411
 412        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 413        """
 414
 415        self.iList = None  # init iList for raw instruments data
 416        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 417        
 418        See also: `Listing()`, `DumpInstruments()`.
 419        """
 420
 421        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 422        if useCache:
 423            if os.path.exists(self.iListDumpFile):
 424                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 425                curTime = datetime.now(tzutc())
 426
 427                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 428                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 429
 430                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 431
 432                else:
 433                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 434
 435                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 436                        os.path.abspath(self.iListDumpFile),
 437                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 438                    ))
 439
 440            else:
 441                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 442                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 443
 444        else:
 445            self.iList = self.Listing()  # request new raw instruments data from broker server
 446            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 447
 448        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 449        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 450
 451        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 452        """
 453
 454    def _ParseJSON(self, rawData="{}") -> dict:
 455        """
 456        Parse JSON from response string.
 457
 458        :param rawData: this is a string with JSON-formatted text.
 459        :return: JSON (dictionary), parsed from server response string.
 460        """
 461        responseJSON = json.loads(rawData) if rawData else {}
 462
 463        if self.moreDebug:
 464            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 465
 466        return responseJSON
 467
 468    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 469        """
 470        Send GET or POST request to broker server and receive JSON object.
 471
 472        self.header: must be defining with dictionary of headers.
 473        self.body: if define then used as request body. None by default.
 474        self.timeout: global request timeout, 15 seconds by default.
 475        :param url: url with REST request.
 476        :param reqType: send "GET" or "POST" request. "GET" by default.
 477        :param retry: how many times retry after first request if an 5xx server errors occurred.
 478        :param pause: sleep time in seconds between retries.
 479        :return: response JSON (dictionary) from broker.
 480        """
 481        if reqType not in ("GET", "POST"):
 482            uLogger.error("You can define request type: 'GET' or 'POST'!")
 483            raise Exception("Incorrect value")
 484
 485        if self.moreDebug:
 486            uLogger.debug("Request parameters:")
 487            uLogger.debug("    - REST API URL: {}".format(url))
 488            uLogger.debug("    - request type: {}".format(reqType))
 489            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 490            uLogger.debug("    - body:\n{}".format(self.body))
 491
 492        # fast hack to avoid all operations with some tickers/FIGI
 493        responseJSON = {}
 494        oK = True
 495        for item in self.exclude:
 496            if item in url:
 497                if self.moreDebug:
 498                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 499
 500                oK = False
 501                break
 502
 503        if oK:
 504            counter = 0
 505            response = None
 506            errMsg = ""
 507
 508            while not response and counter <= retry:
 509                if reqType == "GET":
 510                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 511
 512                if reqType == "POST":
 513                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 514
 515                if self.moreDebug:
 516                    uLogger.debug("Response:")
 517                    uLogger.debug("    - status code: {}".format(response.status_code))
 518                    uLogger.debug("    - reason: {}".format(response.reason))
 519                    uLogger.debug("    - body length: {}".format(len(response.text)))
 520                    uLogger.debug("    - headers:\n{}".format(response.headers))
 521
 522                # Server returns some headers:
 523                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 524                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 525                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 526                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 527                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 528                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 529                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 530                    sleep(rateLimitWait)
 531
 532                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 533                if 400 <= response.status_code < 500:
 534                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 535                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 536                    counter = retry + 1
 537
 538                if 500 <= response.status_code < 600:
 539                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 540                    uLogger.debug("    - not oK, {}".format(errMsg))
 541                    counter += 1
 542
 543                    if counter <= retry:
 544                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 545                        sleep(pause)
 546
 547            responseJSON = self._ParseJSON(rawData=response.text)
 548
 549            if errMsg:
 550                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 551                uLogger.error("    - not oK, {}".format(errMsg))
 552
 553        return responseJSON
 554
 555    def _IUpdater(self, iType: str) -> tuple:
 556        """
 557        Request instrument by type from server. See available API methods for instruments:
 558        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 559        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 560        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 561        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 562        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 563
 564        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 565        :return: tuple with iType name and list of available instruments of current type for defined user token.
 566        """
 567        result = []
 568
 569        if iType in TKS_INSTRUMENTS:
 570            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 571
 572            # all instruments have the same body in API v2 requests:
 573            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 574            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 575            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 576
 577        return iType, result
 578
 579    def _IWrapper(self, kwargs):
 580        """
 581        Wrapper runs instrument's update method `_IUpdater()`.
 582        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 583        """
 584        return self._IUpdater(**kwargs)
 585
 586    def Listing(self) -> dict:
 587        """
 588        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 589
 590        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 591        """
 592        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 593        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 594
 595        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 596        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 597        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 598
 599        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 600        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 601        poolUpdater.close()
 602
 603        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 604        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 605        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 606
 607        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 608        for iType in iList.keys():
 609            for ticker in iList[iType]:
 610                iList[iType][ticker]["type"] = iType
 611
 612                if "minPriceIncrement" in iList[iType][ticker].keys():
 613                    iList[iType][ticker]["step"] = NanoToFloat(
 614                        iList[iType][ticker]["minPriceIncrement"]["units"],
 615                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 616                    )
 617
 618                else:
 619                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 620
 621        return iList
 622
 623    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 624        """
 625        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 626
 627        See also: `DumpInstruments()`, `Listing()`.
 628
 629        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 630                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 631        """
 632        if self.iListDumpFile is None or not self.iListDumpFile:
 633            uLogger.error("Output name of dump file must be defined!")
 634            raise Exception("Filename required")
 635
 636        if not self.iList or forceUpdate:
 637            self.iList = self.Listing()
 638
 639        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 640
 641        # Save as XLSX with separated sheets for every type of instruments:
 642        with pd.ExcelWriter(
 643                path=xlsxDumpFile,
 644                date_format=TKS_DATE_FORMAT,
 645                datetime_format=TKS_DATE_TIME_FORMAT,
 646                mode="w",
 647        ) as writer:
 648            for iType in TKS_INSTRUMENTS:
 649                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 650                df = df[sorted(df)]  # sorted by column names
 651                df = df.applymap(
 652                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 653                    na_action="ignore",
 654                )  # converting numbers from nano-type to float in every cell
 655                df.to_excel(
 656                    writer,
 657                    sheet_name=iType,
 658                    encoding="UTF-8",
 659                    freeze_panes=(1, 1),
 660                )  # saving as XLSX-file with freeze first row and column as headers
 661
 662        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 663
 664    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 665        """
 666        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 667        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 668
 669        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 670
 671        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 672                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 673        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 674        """
 675        if self.iListDumpFile is None or not self.iListDumpFile:
 676            uLogger.error("Output name of dump file must be defined!")
 677            raise Exception("Filename required")
 678
 679        if not self.iList or forceUpdate:
 680            self.iList = self.Listing()
 681
 682        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 683        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 684            fH.write(jsonDump)
 685
 686        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 687
 688        return jsonDump
 689
 690    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 691        """
 692        Show information about one instrument defined by json data and prints it in Markdown format.
 693
 694        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 695
 696        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 697        :param show: if `True` then also printing information about instrument and its current price.
 698        :return: multilines text in Markdown format with information about one instrument.
 699        """
 700        splitLine = "|                                                             |                                                        |\n"
 701        infoText = ""
 702
 703        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 704            info = [
 705                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 706                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 707                "| Parameters                                                  | Values                                                 |\n",
 708                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 709                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 710                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 711            ]
 712
 713            if "sector" in iJSON.keys() and iJSON["sector"]:
 714                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 715
 716            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 717                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 718                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 719            )))
 720
 721            info.extend([
 722                splitLine,
 723                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 724                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 725            ])
 726
 727            if "isin" in iJSON.keys() and iJSON["isin"]:
 728                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 729
 730            if "classCode" in iJSON.keys():
 731                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 732
 733            info.extend([
 734                splitLine,
 735                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 736                splitLine,
 737                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 738                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 739                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 740            ])
 741
 742            if iJSON["figi"]:
 743                self.figi = iJSON["figi"]
 744                iJSON = iJSON | self.RequestTradingStatus()
 745
 746                info.extend([
 747                    splitLine,
 748                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 749                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 750                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 751                ])
 752
 753            info.append(splitLine)
 754
 755            if "type" in iJSON.keys() and iJSON["type"]:
 756                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 757
 758            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 759                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 760
 761            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 762                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 765                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 768                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 769
 770            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 771                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 772
 773            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 774                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 775
 776            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 777                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 778
 779            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 780                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 781
 782            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 783                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 784
 785            if "currency" in iJSON.keys():
 786                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 787
 788            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 789                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 790
 791            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 792                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 793
 794            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 795                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 798                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 801                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 804                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 807                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 810                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 811
 812            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 813                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 814
 815            iExt = None
 816            if iJSON["type"] == "Bonds":
 817                info.extend([
 818                    splitLine,
 819                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 820                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 821                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 822                        iJSON["nominal"]["currency"],
 823                    )),
 824                ])
 825
 826                if "floatingCouponFlag" in iJSON.keys():
 827                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 828
 829                if "amortizationFlag" in iJSON.keys():
 830                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 831
 832                info.append(splitLine)
 833
 834                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 835                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 836
 837                if iJSON["figi"]:
 838                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 839
 840                    info.extend([
 841                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 842                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 843                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 844                    ])
 845
 846                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 847                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 848                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 849                        iJSON["aciValue"]["currency"]
 850                    )))
 851
 852            if "currentPrice" in iJSON.keys():
 853                info.append(splitLine)
 854
 855                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 856                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 857
 858                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 859                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 860                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 861                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 862                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 863
 864                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 865                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 866
 867                info.extend([
 868                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 869                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 870                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 871                    )),
 872                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 873                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 874                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 875                    )),
 876                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 877                        "{:.2f}%{}".format(
 878                            iJSON["currentPrice"]["changes"],
 879                            " ({}{:.2f} {})".format(
 880                                "+" if bondChangesDelta > 0 else "",
 881                                bondChangesDelta,
 882                                aciCurrency
 883                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 884                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 885                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 886                                currency
 887                            ),
 888                        )
 889                    ),
 890                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 891                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 892                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 893                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 894                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 895                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 896                    )),
 897                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 898                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 899                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 900                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 902                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 903                    )),
 904                ])
 905
 906            if "lot" in iJSON.keys():
 907                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 908
 909            if "step" in iJSON.keys() and iJSON["step"] != 0:
 910                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 911
 912            # Add bond payment calendar:
 913            if iJSON["type"] == "Bonds":
 914                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 915                info.extend(["\n", strCalendar])
 916
 917            infoText += "".join(info)
 918
 919            if show:
 920                uLogger.info("{}".format(infoText))
 921
 922            else:
 923                uLogger.debug("{}".format(infoText))
 924
 925            if self.infoFile is not None:
 926                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 927                    fH.write(infoText)
 928
 929                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 930
 931        return infoText
 932
 933    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934        """
 935        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 936
 937        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 938        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 939        :return: JSON formatted data with information about instrument.
 940        """
 941        tickerJSON = {}
 942        if self.moreDebug:
 943            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 944
 945        if not self.ticker:
 946            uLogger.warning("self.ticker variable is not be empty!")
 947
 948        else:
 949            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 950                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 951                raise Exception("Instrument not allowed")
 952
 953            if not self.iList:
 954                self.iList = self.Listing()
 955
 956            if self.ticker in self.iList["Shares"].keys():
 957                tickerJSON = self.iList["Shares"][self.ticker]
 958                if self.moreDebug:
 959                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 960
 961            elif self.ticker in self.iList["Currencies"].keys():
 962                tickerJSON = self.iList["Currencies"][self.ticker]
 963                if self.moreDebug:
 964                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 965
 966            elif self.ticker in self.iList["Bonds"].keys():
 967                tickerJSON = self.iList["Bonds"][self.ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Etfs"].keys():
 972                tickerJSON = self.iList["Etfs"][self.ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Futures"].keys():
 977                tickerJSON = self.iList["Futures"][self.ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 980
 981        if tickerJSON:
 982            self.figi = tickerJSON["figi"]
 983
 984            if requestPrice:
 985                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 986
 987                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 988                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 989
 990                else:
 991                    tickerJSON["currentPrice"]["changes"] = 0
 992
 993            if show:
 994                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 995
 996        else:
 997            if show:
 998                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 999
1000        return tickerJSON
1001
1002    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003        """
1004        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :return: JSON formatted data with information about instrument.
1009        """
1010        figiJSON = {}
1011        if self.moreDebug:
1012            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1013
1014        if not self.figi:
1015            uLogger.warning("self.figi variable is not be empty!")
1016
1017        else:
1018            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1019                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1020                raise Exception("Instrument not allowed")
1021
1022            if not self.iList:
1023                self.iList = self.Listing()
1024
1025            for item in self.iList["Shares"].keys():
1026                if self.figi == self.iList["Shares"][item]["figi"]:
1027                    figiJSON = self.iList["Shares"][item]
1028
1029                    if self.moreDebug:
1030                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1031
1032                    break
1033
1034            if not figiJSON:
1035                for item in self.iList["Currencies"].keys():
1036                    if self.figi == self.iList["Currencies"][item]["figi"]:
1037                        figiJSON = self.iList["Currencies"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1041
1042                        break
1043
1044            if not figiJSON:
1045                for item in self.iList["Bonds"].keys():
1046                    if self.figi == self.iList["Bonds"][item]["figi"]:
1047                        figiJSON = self.iList["Bonds"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Etfs"].keys():
1056                    if self.figi == self.iList["Etfs"][item]["figi"]:
1057                        figiJSON = self.iList["Etfs"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Futures"].keys():
1066                    if self.figi == self.iList["Futures"][item]["figi"]:
1067                        figiJSON = self.iList["Futures"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1071
1072                        break
1073
1074        if figiJSON:
1075            self.figi = figiJSON["figi"]
1076            self.ticker = figiJSON["ticker"]
1077
1078            if requestPrice:
1079                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1080
1081                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1082                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1083
1084                else:
1085                    figiJSON["currentPrice"]["changes"] = 0
1086
1087            if show:
1088                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1089
1090        else:
1091            if show:
1092                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1093
1094        return figiJSON
1095
1096    def GetCurrentPrices(self, show: bool = True) -> dict:
1097        """
1098        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1099        `{"buy": [{"price": 1243.8, "quantity": 193},
1100                  {"price": 1244.0, "quantity": 168},
1101                  {"price": 1244.8, "quantity": 5},
1102                  {"price": 1245.0, "quantity": 61},
1103                  {"price": 1245.4, "quantity": 60}],
1104          "sell": [{"price": 1243.6, "quantity": 8},
1105                   {"price": 1242.6, "quantity": 10},
1106                   {"price": 1242.4, "quantity": 18},
1107                   {"price": 1242.2, "quantity": 50},
1108                   {"price": 1242.0, "quantity": 113}],
1109          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1110        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1111        - sell: list of dicts with Buyers prices,
1112            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1113            - quantity: volume value by current price in lots,
1114        - limitUp: current trade session limit price, maximum,
1115        - limitDown: current trade session limit price, minimum,
1116        - lastPrice: last deal price of the instrument,
1117        - closePrice: previous trade session close price of the instrument.
1118
1119        See also: `SearchByTicker()` and `SearchByFIGI()`.
1120        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1121        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1122
1123        :param show: if `True` then print DOM to log and console.
1124        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1125                 If an error occurred then returns an empty record:
1126                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1127        """
1128        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1129
1130        if self.depth < 1:
1131            uLogger.error("Depth of Market (DOM) must be >=1!")
1132            raise Exception("Incorrect value")
1133
1134        if not (self.ticker or self.figi):
1135            uLogger.error("self.ticker or self.figi variables must be defined!")
1136            raise Exception("Ticker or FIGI required")
1137
1138        if self.ticker and not self.figi:
1139            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1140            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1141
1142        if not self.ticker and self.figi:
1143            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1144            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1145
1146        if not self.figi:
1147            uLogger.error("FIGI is not defined!")
1148            raise Exception("Ticker or FIGI required")
1149
1150        else:
1151            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1152
1153            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1154            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1155            self.body = str({"figi": self.figi, "depth": self.depth})
1156            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1157
1158            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1159                # list of dicts with sellers orders:
1160                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1161
1162                # list of dicts with buyers orders:
1163                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1164
1165                # max price of instrument at this time:
1166                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1167
1168                # min price of instrument at this time:
1169                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1170
1171                # last price of deal with instrument:
1172                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1173
1174                # last close price of instrument:
1175                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1176
1177            else:
1178                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1179                uLogger.debug("Server response: {}".format(pricesResponse))
1180
1181            if show:
1182                if prices["buy"] or prices["sell"]:
1183                    info = [
1184                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1185                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1186                            self.ticker,
1187                            self.figi,
1188                            self.depth,
1189                        ),
1190                        "-" * 60, "\n",
1191                        "             Orders of Buyers | Orders of Sellers\n",
1192                        "-" * 60, "\n",
1193                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1194                        "-" * 60, "\n",
1195                    ]
1196
1197                    if not prices["buy"]:
1198                        info.append("                              | No orders!\n")
1199                        sumBuy = 0
1200
1201                    else:
1202                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1203                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1204                        for item in maxMinSorted:
1205                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1206
1207                    if not prices["sell"]:
1208                        info.append("No orders!                    |\n")
1209                        sumSell = 0
1210
1211                    else:
1212                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1213                        for item in prices["sell"]:
1214                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1215
1216                    info.extend([
1217                        "-" * 60, "\n",
1218                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1219                        "-" * 60, "\n",
1220                    ])
1221
1222                    infoText = "".join(info)
1223
1224                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1225
1226                else:
1227                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1228
1229        return prices
1230
1231    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1232        """
1233        This method get and show information about all available broker instruments for current user account.
1234        If `instrumentsFile` string is not empty then also save information to this file.
1235
1236        :param show: if `True` then print results to console, if `False` - print only to file.
1237        :return: multi-lines string with all available broker instruments
1238        """
1239        if not self.iList:
1240            self.iList = self.Listing()
1241
1242        info = [
1243            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1244            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1245        ]
1246
1247        # add instruments count by type:
1248        for iType in self.iList.keys():
1249            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1250
1251        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1252        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1253
1254        # generating info tables with all instruments by type:
1255        for iType in self.iList.keys():
1256            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1257
1258            for instrument in self.iList[iType].keys():
1259                iName = self.iList[iType][instrument]["name"]  # instrument's name
1260                if len(iName) > 57:
1261                    iName = "{}...".format(iName[:54])  # right trim for a long string
1262
1263                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1264                    self.iList[iType][instrument]["ticker"],
1265                    iName,
1266                    self.iList[iType][instrument]["figi"],
1267                    self.iList[iType][instrument]["currency"],
1268                    self.iList[iType][instrument]["lot"],
1269                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1270                ))
1271
1272        infoText = "".join(info)
1273
1274        if show:
1275            uLogger.info(infoText)
1276
1277        if self.instrumentsFile:
1278            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1279                fH.write(infoText)
1280
1281            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1282
1283        return infoText
1284
1285    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1286        """
1287        This method search and show information about instruments by part of its ticker, FIGI or name.
1288        If `searchResultsFile` string is not empty then also save information to this file.
1289
1290        :param pattern: string with part of ticker, FIGI or instrument's name.
1291        :param show: if `True` then print results to console, if `False` - return list of result only.
1292        :return: list of dictionaries with all found instruments.
1293        """
1294        if not self.iList:
1295            self.iList = self.Listing()
1296
1297        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1298        compiledPattern = re.compile(pattern, re.IGNORECASE)
1299
1300        for iType in self.iList:
1301            for instrument in self.iList[iType].values():
1302                searchResult = compiledPattern.search(" ".join(
1303                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1304                ))
1305
1306                if searchResult:
1307                    searchResults[iType][instrument["ticker"]] = instrument
1308
1309        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1310        info = [
1311            "# Search results\n\n",
1312            "* **Search pattern:** [{}]\n".format(pattern),
1313            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1314            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1315        ]
1316        infoShort = info[:]
1317
1318        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1319        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1320        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1321
1322        if resultsLen == 0:
1323            info.append("\nNo results\n")
1324            infoShort.append("\nNo results\n")
1325            uLogger.warning("No results. Try changing your search pattern.")
1326
1327        else:
1328            for iType in searchResults:
1329                iTypeValuesCount = len(searchResults[iType].values())
1330                if iTypeValuesCount > 0:
1331                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1332                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333
1334                    for instrument in searchResults[iType].values():
1335                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1336                            instrument["type"],
1337                            instrument["ticker"],
1338                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1339                            instrument["figi"],
1340                        ))
1341
1342                    if iTypeValuesCount <= 5:
1343                        infoShort.extend(info[-iTypeValuesCount:])
1344
1345                    else:
1346                        infoShort.extend(info[-5:])
1347                        infoShort.append(skippedLine)
1348
1349        infoText = "".join(info)
1350        infoTextShort = "".join(infoShort)
1351
1352        if show:
1353            uLogger.info(infoTextShort)
1354            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1355
1356        if self.searchResultsFile:
1357            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1358                fH.write(infoText)
1359
1360            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1361
1362        return searchResults
1363
1364    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1365        """
1366        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1367
1368        :param instruments: list of strings with tickers or FIGIs.
1369        :return: list with unique instrument FIGIs only.
1370        """
1371        requestedInstruments = []
1372        for iName in instruments:
1373            if iName not in self.aliases.keys():
1374                if iName not in requestedInstruments:
1375                    requestedInstruments.append(iName)
1376
1377            else:
1378                if iName not in requestedInstruments:
1379                    if self.aliases[iName] not in requestedInstruments:
1380                        requestedInstruments.append(self.aliases[iName])
1381
1382        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1383
1384        onlyUniqueFIGIs = []
1385        for iName in requestedInstruments:
1386            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1387                continue
1388
1389            self.ticker = iName
1390            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1391
1392            if not iData:
1393                self.ticker = ""
1394                self.figi = iName
1395
1396                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1397
1398                if not iData:
1399                    self.figi = ""
1400                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1401
1402            if iData and iData["figi"] not in onlyUniqueFIGIs:
1403                onlyUniqueFIGIs.append(iData["figi"])
1404
1405        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1406
1407        return onlyUniqueFIGIs
1408
1409    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1410        """
1411        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1412        See limits: https://tinkoff.github.io/investAPI/limits/
1413        If `pricesFile` string is not empty then also save information to this file.
1414
1415        :param instruments: list of strings with tickers or FIGIs.
1416        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1417        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1418                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1419        """
1420        if instruments is None or not instruments:
1421            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1422            raise Exception("Ticker or FIGI required")
1423
1424        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1425
1426        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1427
1428        iList = []  # trying to get info and current prices about all unique instruments:
1429        for self.figi in onlyUniqueFIGIs:
1430            iData = self.SearchByFIGI(requestPrice=True)
1431            iList.append(iData)
1432
1433        self.ShowListOfPrices(iList, show)
1434
1435        return iList
1436
1437    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1438        """
1439        Show table contains current prices of given instruments.
1440
1441        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1442                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1443        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1444        :return: multilines text in Markdown format as a table contains current prices.
1445        """
1446        infoText = ""
1447
1448        if show or self.pricesFile:
1449            info = [
1450                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1451                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1452                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1453            ]
1454
1455            for item in iList:
1456                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1457                    item["ticker"],
1458                    item["figi"],
1459                    item["type"],
1460                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1461                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1462                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1463                    "{} / {}".format(
1464                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1465                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1466                    ),
1467                    "{} / {}".format(
1468                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1469                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1470                    ),
1471                    item["currency"],
1472                ))
1473
1474            infoText = "".join(info)
1475
1476            if show:
1477                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1478
1479            if self.pricesFile:
1480                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1481                    fH.write(infoText)
1482
1483                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1484
1485        return infoText
1486
1487    def RequestTradingStatus(self) -> dict:
1488        """
1489        Requesting trading status for the instrument defined by `figi` variable.
1490        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1491        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1492
1493        :return: dictionary with trading status attributes. Response example:
1494                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1495                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1496        """
1497        if self.figi is None or not self.figi:
1498            uLogger.error("Variable `figi` must be defined for using this method!")
1499            raise Exception("FIGI required")
1500
1501        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1502
1503        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1504        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1505        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1506
1507        if self.moreDebug:
1508            uLogger.debug("Records about current trading status successfully received")
1509
1510        return tradingStatus
1511
1512    def RequestPortfolio(self) -> dict:
1513        """
1514        Requesting actual user's portfolio for current `accountId`.
1515        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1516        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1517
1518        :return: dictionary with user's portfolio.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1528        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1529
1530        if self.moreDebug:
1531            uLogger.debug("Records about user's portfolio successfully received")
1532
1533        return rawPortfolio
1534
1535    def RequestPositions(self) -> dict:
1536        """
1537        Requesting open positions by currencies and instruments for current `accountId`.
1538        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1539        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1540
1541        :return: dictionary with open positions by instruments.
1542        """
1543        if self.accountId is None or not self.accountId:
1544            uLogger.error("Variable `accountId` must be defined for using this method!")
1545            raise Exception("Account ID required")
1546
1547        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1548
1549        self.body = str({"accountId": self.accountId})
1550        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1551        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1552
1553        if self.moreDebug:
1554            uLogger.debug("Records about current open positions successfully received")
1555
1556        return rawPositions
1557
1558    def RequestPendingOrders(self) -> list:
1559        """
1560        Requesting current actual pending orders for current `accountId`.
1561        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1562        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1563
1564        :return: list of dictionaries with pending orders.
1565        """
1566        if self.accountId is None or not self.accountId:
1567            uLogger.error("Variable `accountId` must be defined for using this method!")
1568            raise Exception("Account ID required")
1569
1570        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1571
1572        self.body = str({"accountId": self.accountId})
1573        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1574        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1575
1576        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1577
1578        return rawOrders
1579
1580    def RequestStopOrders(self) -> list:
1581        """
1582        Requesting current actual stop orders for current `accountId`.
1583        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1584        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1585
1586        :return: list of dictionaries with stop orders.
1587        """
1588        if self.accountId is None or not self.accountId:
1589            uLogger.error("Variable `accountId` must be defined for using this method!")
1590            raise Exception("Account ID required")
1591
1592        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1593
1594        self.body = str({"accountId": self.accountId})
1595        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1596        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1597
1598        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1599
1600        return rawStopOrders
1601
1602    def Overview(self, show: bool = False, details: str = "full") -> dict:
1603        """
1604        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1605        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1606        are defined then also save information to file.
1607
1608        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1609        many requests about the state of the portfolio, and then, based on the received data, a large number
1610        of calculation and statistics are collected.
1611
1612        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1613        :param details: how detailed should the information be? You should specify one of strings:
1614                        `full` - shows full available information about portfolio status (by default),
1615                        `positions` - shows only open positions,
1616                        `digest` - show a short digest of the portfolio status,
1617                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1618                        `orders` - shows only sections of open limits and stop orders.
1619        :return: dictionary with client's raw portfolio and some statistics.
1620        """
1621        if self.accountId is None or not self.accountId:
1622            uLogger.error("Variable `accountId` must be defined for using this method!")
1623            raise Exception("Account ID required")
1624
1625        view = {
1626            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1627                "headers": {},  # list of dictionaries, response headers without "positions" section
1628                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1629                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1630                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1631                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1632                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1633                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1634                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1635                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1636                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1637            },
1638            "stat": {  # --- some statistics calculated using "raw" sections:
1639                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1640                "availableRUB": 0.,  # available rubles (without other currencies)
1641                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1642                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1643                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1644                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1645                "sharesCostRUB": 0.,  # costs of all shares in RUB
1646                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1647                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1648                "futuresCostRUB": 0.,  # costs of all futures in RUB
1649                "Currencies": [],  # list of dictionaries of all currencies statistics
1650                "Shares": [],  # list of dictionaries of all shares statistics
1651                "Bonds": [],  # list of dictionaries of all bonds statistics
1652                "Etfs": [],  # list of dictionaries of all etfs statistics
1653                "Futures": [],  # list of dictionaries of all futures statistics
1654                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1655                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1656                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1657                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1658                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1659            },
1660            "analytics": {  # --- some analytics of portfolio:
1661                "distrByAssets": {},  # portfolio distribution by assets
1662                "distrByCompanies": {},  # portfolio distribution by companies
1663                "distrBySectors": {},  # portfolio distribution by sectors
1664                "distrByCurrencies": {},  # portfolio distribution by currencies
1665                "distrByCountries": {},  # portfolio distribution by countries
1666            }
1667        }
1668
1669        details = details.lower()
1670        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1671        if details not in availableDetails:
1672            details = "full"
1673            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1674
1675        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1676
1677        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1678        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1679        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1680        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1681
1682        # save response headers without "positions" section:
1683        for key in portfolioResponse.keys():
1684            if key != "positions":
1685                view["raw"]["headers"][key] = portfolioResponse[key]
1686
1687            else:
1688                continue
1689
1690        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1691        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1692        for item in portfolioResponse["positions"]:
1693            if item["instrumentType"] == "currency":
1694                self.figi = item["figi"]
1695                curr = self.SearchByFIGI(requestPrice=False)
1696
1697                # current price of currency in RUB:
1698                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1699                    "name": curr["name"],
1700                    "currentPrice": NanoToFloat(
1701                        item["currentPrice"]["units"],
1702                        item["currentPrice"]["nano"]
1703                    ),
1704                }
1705
1706                view["raw"]["Currencies"].append(item)
1707
1708            elif item["instrumentType"] == "share":
1709                view["raw"]["Shares"].append(item)
1710
1711            elif item["instrumentType"] == "bond":
1712                view["raw"]["Bonds"].append(item)
1713
1714            elif item["instrumentType"] == "etf":
1715                view["raw"]["Etfs"].append(item)
1716
1717            elif item["instrumentType"] == "futures":
1718                view["raw"]["Futures"].append(item)
1719
1720            else:
1721                continue
1722
1723        # how many volume of currencies (by ISO currency name) are blocked:
1724        for item in view["raw"]["positions"]["blocked"]:
1725            blocked = NanoToFloat(item["units"], item["nano"])
1726            if blocked > 0:
1727                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1728
1729        # how many volume of instruments (by FIGI) are blocked:
1730        for item in view["raw"]["positions"]["securities"]:
1731            blocked = int(item["blocked"])
1732            if blocked > 0:
1733                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1734
1735        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1736
1737        if "rub" in allBlocked.keys():
1738            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1739
1740        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1741        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1742        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1743        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1744        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1745        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1746        view["stat"]["portfolioCostRUB"] = sum([
1747            view["stat"]["allCurrenciesCostRUB"],
1748            view["stat"]["sharesCostRUB"],
1749            view["stat"]["bondsCostRUB"],
1750            view["stat"]["etfsCostRUB"],
1751            view["stat"]["futuresCostRUB"],
1752        ])
1753
1754        # --- calculating some portfolio statistics:
1755        byComp = {}  # distribution by companies
1756        bySect = {}  # distribution by sectors
1757        byCurr = {}  # distribution by currencies (include RUB)
1758        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1759        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1760
1761        for item in portfolioResponse["positions"]:
1762            self.figi = item["figi"]
1763            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1764
1765            if instrument:
1766                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1767                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1768
1769                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1770                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1771
1772                else:
1773                    blocked = 0
1774
1775                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1776                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1777                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1778                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1779                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1780                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1781                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1782                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1783                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1784                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1785                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1786                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1787
1788                statData = {
1789                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1790                    "ticker": instrument["ticker"],  # ticker by FIGI
1791                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1792                    "volume": volume,  # available volume of instrument
1793                    "lots": lots,  # volume in lots of instrument
1794                    "direction": direction,  # direction of an instrument's position: short or long
1795                    "blocked": blocked,  # blocked volume of currency or instrument
1796                    "currentPrice": curPrice,  # current instrument's price in basic asset
1797                    "average": average,  # current average position price
1798                    "cost": cost,  # current cost of all volume of instrument in basic asset
1799                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1800                    "costRUB": costRUB,  # cost of instrument in ruble
1801                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1802                    "profit": profit,  # expected profit at current moment
1803                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1804                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1805                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1806                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1807                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1808                    "step": instrument["step"],  # minimum price increment
1809                }
1810
1811                # adding distribution by unique countries:
1812                if statData["country"] not in byCountry.keys():
1813                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1814
1815                else:
1816                    byCountry[statData["country"]]["cost"] += costRUB
1817                    byCountry[statData["country"]]["percent"] += percentCostRUB
1818
1819                if item["instrumentType"] != "currency":
1820                    # adding distribution by unique companies:
1821                    if statData["name"]:
1822                        if statData["name"] not in byComp.keys():
1823                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1824
1825                        else:
1826                            byComp[statData["name"]]["cost"] += costRUB
1827                            byComp[statData["name"]]["percent"] += percentCostRUB
1828
1829                    # adding distribution by unique sectors:
1830                    if statData["sector"] not in bySect.keys():
1831                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1832
1833                    else:
1834                        bySect[statData["sector"]]["cost"] += costRUB
1835                        bySect[statData["sector"]]["percent"] += percentCostRUB
1836
1837                # adding distribution by unique currencies:
1838                if currency not in byCurr.keys():
1839                    byCurr[currency] = {
1840                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1841                        "cost": costRUB,
1842                        "percent": percentCostRUB
1843                    }
1844
1845                else:
1846                    byCurr[currency]["cost"] += costRUB
1847                    byCurr[currency]["percent"] += percentCostRUB
1848
1849                # saving statistics for every instrument:
1850                if item["instrumentType"] == "currency":
1851                    view["stat"]["Currencies"].append(statData)
1852
1853                    # update dict with free funds for trading (total - blocked) by currencies
1854                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1855                    view["stat"]["funds"][currency] = {
1856                        "total": volume,
1857                        "totalCostRUB": costRUB,  # total volume cost in rubles
1858                        "free": volume - blocked,
1859                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1860                    }
1861
1862                elif item["instrumentType"] == "share":
1863                    view["stat"]["Shares"].append(statData)
1864
1865                elif item["instrumentType"] == "bond":
1866                    view["stat"]["Bonds"].append(statData)
1867
1868                elif item["instrumentType"] == "etf":
1869                    view["stat"]["Etfs"].append(statData)
1870
1871                elif item["instrumentType"] == "Futures":
1872                    view["stat"]["Futures"].append(statData)
1873
1874                else:
1875                    continue
1876
1877        # total changes in Russian Ruble:
1878        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1879        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1880        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1881        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1882        view["stat"]["funds"]["rub"] = {
1883            "total": view["stat"]["availableRUB"],
1884            "totalCostRUB": view["stat"]["availableRUB"],
1885            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1886            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887        }
1888
1889        # --- pending orders sector data:
1890        uniquePendingOrders = []
1891        uniquePendingOrdersFIGIs = []
1892        for item in view["raw"]["orders"]:
1893            if item["figi"] not in uniquePendingOrdersFIGIs:
1894                uniquePendingOrdersFIGIs.append(item["figi"])
1895                uniquePendingOrders.append(item)
1896
1897        for item in uniquePendingOrders:
1898            self.figi = item["figi"]
1899            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1900
1901            if instrument:
1902                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1903                orderType = TKS_ORDER_TYPES[item["orderType"]]
1904                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1905                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1906
1907                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1908                if item["direction"] == "ORDER_DIRECTION_BUY":
1909                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1910
1911                else:
1912                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1913
1914                # requested price for order execution:
1915                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1916
1917                # necessary changes in percent to reach target from current price:
1918                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1919
1920                view["stat"]["orders"].append({
1921                    "orderID": item["orderId"],  # orderId number parameter of current order
1922                    "figi": item["figi"],  # FIGI identification
1923                    "ticker": instrument["ticker"],  # ticker name by FIGI
1924                    "lotsRequested": item["lotsRequested"],  # requested lots value
1925                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1926                    "currentPrice": lastPrice,  # current instrument's price for defined action
1927                    "targetPrice": target,  # requested price for order execution in base currency
1928                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1929                    "percentChanges": changes,  # changes in percent to target from current price
1930                    "currency": item["currency"],  # instrument's currency name
1931                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1932                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1933                    "status": orderState,  # order status from TKS_ORDER_STATES
1934                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1935                })
1936
1937        # --- stop orders sector data:
1938        uniqueStopOrders = []
1939        uniqueStopOrdersFIGIs = []
1940        for item in view["raw"]["stopOrders"]:
1941            if item["figi"] not in uniqueStopOrdersFIGIs:
1942                uniqueStopOrdersFIGIs.append(item["figi"])
1943                uniqueStopOrders.append(item)
1944
1945        for item in uniqueStopOrders:
1946            self.figi = item["figi"]
1947            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1948
1949            if instrument:
1950                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1951                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1952                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1953
1954                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1955                if "expirationTime" in item.keys():
1956                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1957                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1958
1959                else:
1960                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1961                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1962
1963                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1964                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1965                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1966
1967                else:
1968                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1969
1970                # requested price when stop-order executed:
1971                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1972
1973                # price for limit-order, set up when stop-order executed:
1974                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1975
1976                # necessary changes in percent to reach target from current price:
1977                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1978
1979                view["stat"]["stopOrders"].append({
1980                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1981                    "figi": item["figi"],  # FIGI identification
1982                    "ticker": instrument["ticker"],  # ticker name by FIGI
1983                    "lotsRequested": item["lotsRequested"],  # requested lots value
1984                    "currentPrice": lastPrice,  # current instrument's price for defined action
1985                    "targetPrice": target,  # requested price for stop-order execution in base currency
1986                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1987                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1988                    "percentChanges": changes,  # changes in percent to target from current price
1989                    "currency": item["currency"],  # instrument's currency name
1990                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1991                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1992                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1993                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1994                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1995                })
1996
1997        # --- calculating data for analytics section:
1998        # portfolio distribution by assets:
1999        view["analytics"]["distrByAssets"] = {
2000            "Ruble": {
2001                "uniques": 1,
2002                "cost": view["stat"]["availableRUB"],
2003                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004            },
2005            "Currencies": {
2006                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2007                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2008                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2009            },
2010            "Shares": {
2011                "uniques": len(view["stat"]["Shares"]),
2012                "cost": view["stat"]["sharesCostRUB"],
2013                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2014            },
2015            "Bonds": {
2016                "uniques": len(view["stat"]["Bonds"]),
2017                "cost": view["stat"]["bondsCostRUB"],
2018                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2019            },
2020            "Etfs": {
2021                "uniques": len(view["stat"]["Etfs"]),
2022                "cost": view["stat"]["etfsCostRUB"],
2023                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2024            },
2025            "Futures": {
2026                "uniques": len(view["stat"]["Futures"]),
2027                "cost": view["stat"]["futuresCostRUB"],
2028                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2029            },
2030        }
2031
2032        # portfolio distribution by companies:
2033        view["analytics"]["distrByCompanies"]["All money cash"] = {
2034            "ticker": "",
2035            "cost": view["stat"]["allCurrenciesCostRUB"],
2036            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2037        }
2038        view["analytics"]["distrByCompanies"].update(byComp)
2039
2040        # portfolio distribution by sectors:
2041        view["analytics"]["distrBySectors"]["All money cash"] = {
2042            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2043            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2044        }
2045        view["analytics"]["distrBySectors"].update(bySect)
2046
2047        # portfolio distribution by currencies:
2048        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2049            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2050            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2051
2052        view["analytics"]["distrByCurrencies"].update(byCurr)
2053        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2054        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2055
2056        # portfolio distribution by countries:
2057        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2058            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2059            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2060
2061        view["analytics"]["distrByCountries"].update(byCountry)
2062        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2063        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2064
2065        # --- Prepare text statistics overview in human-readable:
2066        if show:
2067            # Whatever the value `details`, header not changes:
2068            info = [
2069                "# Client's portfolio\n\n",
2070                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2071                "* **Account ID:** [{}]\n".format(self.accountId),
2072            ]
2073
2074            if details in ["full", "positions", "digest"]:
2075                info.extend([
2076                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2077                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2078                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2079                        view["stat"]["totalChangesRUB"],
2080                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2081                        view["stat"]["totalChangesPercentRUB"],
2082                    ),
2083                ])
2084
2085            if details in ["full", "positions"]:
2086                info.extend([
2087                    "## Open positions\n\n",
2088                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2089                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2090                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2091                        "{:.2f} ({:.2f}) rub".format(
2092                            view["stat"]["availableRUB"],
2093                            view["stat"]["blockedRUB"],
2094                        )
2095                    )
2096                ])
2097
2098                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2099                    return [
2100                        "|                             |                                 |          |              |              |                     |                              |\n",
2101                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2102                            noTradeStr if noTradeStr else typeStr,
2103                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2104                        ),
2105                    ]
2106
2107                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2108                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2109                        "{} [{}]".format(data["ticker"], data["figi"]),
2110                        "{:.2f} ({:.2f}) {}".format(
2111                            data["volume"],
2112                            data["blocked"],
2113                            data["currency"],
2114                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2115                            data["volume"],
2116                            data["blocked"],
2117                        ),
2118                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2119                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2120                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2121                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2122                        "{}{:.2f} {} ({}{:.2f}%)".format(
2123                            "+" if data["profit"] > 0 else "",
2124                            data["profit"], data["baseCurrencyName"],
2125                            "+" if data["percentProfit"] > 0 else "",
2126                            data["percentProfit"],
2127                        ),
2128                    )
2129
2130                # --- Show currencies section:
2131                if view["stat"]["Currencies"]:
2132                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2133                    for item in view["stat"]["Currencies"]:
2134                        info.append(_InfoStr(item, showCurrencyName=True))
2135
2136                else:
2137                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2138
2139                # --- Show shares section:
2140                if view["stat"]["Shares"]:
2141                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2142
2143                    for item in view["stat"]["Shares"]:
2144                        info.append(_InfoStr(item))
2145
2146                else:
2147                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2148
2149                # --- Show bonds section:
2150                if view["stat"]["Bonds"]:
2151                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2152
2153                    for item in view["stat"]["Bonds"]:
2154                        info.append(_InfoStr(item))
2155
2156                else:
2157                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2158
2159                # --- Show etfs section:
2160                if view["stat"]["Etfs"]:
2161                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2162
2163                    for item in view["stat"]["Etfs"]:
2164                        info.append(_InfoStr(item))
2165
2166                else:
2167                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2168
2169                # --- Show futures section:
2170                if view["stat"]["Futures"]:
2171                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2172
2173                    for item in view["stat"]["Futures"]:
2174                        info.append(_InfoStr(item))
2175
2176                else:
2177                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2178
2179            if details in ["full", "orders"]:
2180                # --- Show pending orders section:
2181                if view["stat"]["orders"]:
2182                    info.extend([
2183                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2184                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2185                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2186                    ])
2187
2188                    for item in view["stat"]["orders"]:
2189                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2190                            "{} [{}]".format(item["ticker"], item["figi"]),
2191                            item["orderID"],
2192                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2193                            "{} {} ({}{:.2f}%)".format(
2194                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2195                                item["baseCurrencyName"],
2196                                "+" if item["percentChanges"] > 0 else "",
2197                                float(item["percentChanges"]),
2198                            ),
2199                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2200                            item["action"],
2201                            item["type"],
2202                            item["date"],
2203                        ))
2204
2205                else:
2206                    info.append("\n## Total pending limit-orders: 0\n")
2207
2208                # --- Show stop orders section:
2209                if view["stat"]["stopOrders"]:
2210                    info.extend([
2211                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2212                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2213                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2214                    ])
2215
2216                    for item in view["stat"]["stopOrders"]:
2217                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2218                            "{} [{}]".format(item["ticker"], item["figi"]),
2219                            item["orderID"],
2220                            item["lotsRequested"],
2221                            "{} {} ({}{:.2f}%)".format(
2222                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2223                                item["baseCurrencyName"],
2224                                "+" if item["percentChanges"] > 0 else "",
2225                                float(item["percentChanges"]),
2226                            ),
2227                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2228                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2229                            item["action"],
2230                            item["type"],
2231                            item["expType"],
2232                            item["createDate"],
2233                            item["expDate"],
2234                        ))
2235
2236                else:
2237                    info.append("\n## Total stop-orders: 0\n")
2238
2239            if details in ["full", "analytics"]:
2240                # -- Show analytics section:
2241                if view["stat"]["portfolioCostRUB"] > 0:
2242                    info.extend([
2243                        "\n# Analytics\n"
2244                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2245                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2246                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2247                            view["stat"]["totalChangesRUB"],
2248                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2249                            view["stat"]["totalChangesPercentRUB"],
2250                        ),
2251                        "\n## Portfolio distribution by assets\n"
2252                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2253                        "|------------|---------|---------|--------------------|\n",
2254                    ])
2255
2256                    for key in view["analytics"]["distrByAssets"].keys():
2257                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2258                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2259                                key,
2260                                view["analytics"]["distrByAssets"][key]["uniques"],
2261                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2262                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2263                            ))
2264
2265                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2266                    info.extend([
2267                        "\n## Portfolio distribution by companies\n"
2268                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2269                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2270                    ])
2271
2272                    for company in view["analytics"]["distrByCompanies"].keys():
2273                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2274                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2275                            info.append("| {} | {:<7} | {:<18} |\n".format(
2276                                "{}{}{}".format(
2277                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2278                                    company,
2279                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2280                                ),
2281                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2282                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2283                            ))
2284
2285                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2286                    info.extend([
2287                        "\n## Portfolio distribution by sectors\n"
2288                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2289                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2290                    ])
2291
2292                    for sector in view["analytics"]["distrBySectors"].keys():
2293                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2294                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2295                                sector,
2296                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2297                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2299                            ))
2300
2301                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2302                    info.extend([
2303                        "\n## Portfolio distribution by currencies\n"
2304                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2305                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2306                    ])
2307
2308                    for curr in view["analytics"]["distrByCurrencies"].keys():
2309                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2310                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2311                            info.append("| {} | {:<7} | {:<18} |\n".format(
2312                                "[{}] {}{}".format(
2313                                    curr,
2314                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2315                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2316                                ),
2317                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2318                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2319                            ))
2320
2321                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2322                    info.extend([
2323                        "\n## Portfolio distribution by countries\n"
2324                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2325                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2326                    ])
2327
2328                    for country in view["analytics"]["distrByCountries"].keys():
2329                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2330                            nameLen = len(country)
2331                            info.append("| {} | {:<7} | {:<18} |\n".format(
2332                                "{}{}".format(
2333                                    country,
2334                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2335                                ),
2336                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2338                            ))
2339
2340            infoText = "".join(info)
2341
2342            uLogger.info(infoText)
2343
2344            if details == "full" and self.overviewFile:
2345                filename = self.overviewFile
2346
2347            elif details == "digest" and self.overviewDigestFile:
2348                filename = self.overviewDigestFile
2349
2350            elif details == "positions" and self.overviewPositionsFile:
2351                filename = self.overviewPositionsFile
2352
2353            elif details == "orders" and self.overviewOrdersFile:
2354                filename = self.overviewOrdersFile
2355
2356            elif details == "analytics" and self.overviewAnalyticsFile:
2357                filename = self.overviewAnalyticsFile
2358
2359            else:
2360                filename = ""
2361
2362            if filename:
2363                with open(filename, "w", encoding="UTF-8") as fH:
2364                    fH.write(infoText)
2365
2366                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2367
2368        return view
2369
2370    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2371        """
2372        Returns history operations between two given dates for current `accountId`.
2373        If `reportFile` string is not empty then also save human-readable report.
2374        Shows some statistical data of closed positions.
2375
2376        :param start: see docstring in `GetDatesAsString()` method
2377        :param end: see docstring in `GetDatesAsString()` method
2378        :param show: if `True` then also prints all records to the console.
2379        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2380        :return: original list of dictionaries with history of deals records from API ("operations" key):
2381                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2382                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2383        """
2384        if self.accountId is None or not self.accountId:
2385            uLogger.error("Variable `accountId` must be defined for using this method!")
2386            raise Exception("Account ID required")
2387
2388        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2389
2390        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2391
2392        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2393        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2394        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2395        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2396        customStat = {}  # custom statistics in additional to responseJSON
2397
2398        # --- output report in human-readable format:
2399        if show or self.reportFile:
2400            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2401            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2402            nextDay = ""
2403
2404            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2405
2406            if len(ops) > 0:
2407                customStat = {
2408                    "opsCount": 0,  # total operations count
2409                    "buyCount": 0,  # buy operations
2410                    "sellCount": 0,  # sell operations
2411                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2412                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2413                    "payIn": {"rub": 0.},  # Deposit brokerage account
2414                    "payOut": {"rub": 0.},  # Withdrawals
2415                    "divs": {"rub": 0.},  # Dividends income
2416                    "coupons": {"rub": 0.},  # Coupon's income
2417                    "brokerCom": {"rub": 0.},  # Service commissions
2418                    "serviceCom": {"rub": 0.},  # Service commissions
2419                    "marginCom": {"rub": 0.},  # Margin commissions
2420                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2421                }
2422
2423                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2424                for item in ops:
2425                    if item["state"] == "OPERATION_STATE_EXECUTED":
2426                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2427
2428                        # count buy operations:
2429                        if "_BUY" in item["operationType"]:
2430                            customStat["buyCount"] += 1
2431
2432                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2433                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2434
2435                            else:
2436                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2437
2438                        # count sell operations:
2439                        elif "_SELL" in item["operationType"]:
2440                            customStat["sellCount"] += 1
2441
2442                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2443                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2444
2445                            else:
2446                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2447
2448                        # count incoming operations:
2449                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2450                            if item["payment"]["currency"] in customStat["payIn"].keys():
2451                                customStat["payIn"][item["payment"]["currency"]] += payment
2452
2453                            else:
2454                                customStat["payIn"][item["payment"]["currency"]] = payment
2455
2456                        # count withdrawals operations:
2457                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2458                            if item["payment"]["currency"] in customStat["payOut"].keys():
2459                                customStat["payOut"][item["payment"]["currency"]] += payment
2460
2461                            else:
2462                                customStat["payOut"][item["payment"]["currency"]] = payment
2463
2464                        # count dividends income:
2465                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2466                            if item["payment"]["currency"] in customStat["divs"].keys():
2467                                customStat["divs"][item["payment"]["currency"]] += payment
2468
2469                            else:
2470                                customStat["divs"][item["payment"]["currency"]] = payment
2471
2472                        # count coupon's income:
2473                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2474                            if item["payment"]["currency"] in customStat["coupons"].keys():
2475                                customStat["coupons"][item["payment"]["currency"]] += payment
2476
2477                            else:
2478                                customStat["coupons"][item["payment"]["currency"]] = payment
2479
2480                        # count broker commissions:
2481                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2482                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2483                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2484
2485                            else:
2486                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2487
2488                        # count service commissions:
2489                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2490                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2491                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2492
2493                            else:
2494                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2495
2496                        # count margin commissions:
2497                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2498                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2499                                customStat["marginCom"][item["payment"]["currency"]] += payment
2500
2501                            else:
2502                                customStat["marginCom"][item["payment"]["currency"]] = payment
2503
2504                        # count withholding taxes:
2505                        elif "_TAX" in item["operationType"]:
2506                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2507                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2508
2509                            else:
2510                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2511
2512                        else:
2513                            continue
2514
2515                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2516
2517                # --- view "Actions" lines:
2518                info.extend([
2519                    "| Report sections            |                               |                              |                      |                        |\n",
2520                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2521                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2522                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2523                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2524                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2525                    ),
2526                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2527                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2528                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2529                    ),
2530                ])
2531
2532                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2533                for key in opsKeys:
2534                    if key == "rub":
2535                        continue
2536
2537                    info.extend([
2538                        "|                            |                               | {:<28} |                      |                        |\n".format(
2539                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2540                        ),
2541                        "|                            |                               | {:<28} |                      |                        |\n".format(
2542                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2543                        ),
2544                    ])
2545
2546                info.append(splitLine1)
2547
2548                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2549                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2550                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2551                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2552                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2553                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2554                    )
2555
2556                # --- view "Payments" lines:
2557                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2558                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2559
2560                for key in paymentsKeys:
2561                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2562
2563                info.append(splitLine1)
2564
2565                # --- view "Commissions and taxes" lines:
2566                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2567                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2568
2569                for key in comKeys:
2570                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2571
2572                info.append(splitLine1)
2573
2574                info.extend([
2575                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2576                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2577                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2578                ])
2579
2580            else:
2581                info.append("Broker returned no operations during this period\n")
2582
2583            # --- view "Operations" section:
2584            for item in ops:
2585                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2586                    continue
2587
2588                else:
2589                    self.figi = item["figi"] if item["figi"] else ""
2590                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2591                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2592
2593                    # group of deals during one day:
2594                    if nextDay and item["date"].split("T")[0] != nextDay:
2595                        info.append(splitLine2)
2596                        nextDay = ""
2597
2598                    else:
2599                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2600
2601                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2602                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2603                        self.figi if self.figi else "—",
2604                        instrument["ticker"] if instrument else "—",
2605                        instrument["type"] if instrument else "—",
2606                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2607                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2608                        TKS_OPERATION_STATES[item["state"]],
2609                        TKS_OPERATION_TYPES[item["operationType"]],
2610                    ))
2611
2612            infoText = "".join(info)
2613
2614            if show:
2615                if self.moreDebug:
2616                    uLogger.debug("Records about history of a client's operations successfully received")
2617
2618                uLogger.info(infoText)
2619
2620            if self.reportFile:
2621                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2622                    fH.write(infoText)
2623
2624                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2625
2626        return ops, customStat
2627
2628    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2629        """
2630        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2631
2632        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2633        Warning! Broker server used ISO UTC time by default.
2634
2635        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2636        Also, `historyFile` used to update history with `onlyMissing` parameter.
2637
2638        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2639
2640        :param start: see docstring in `GetDatesAsString()` method.
2641        :param end: see docstring in `GetDatesAsString()` method.
2642        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2643                         `"hour"`, `"day"`. Default: `"hour"`.
2644        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2645                            False by default. Warning! History appends only from last candle to current time
2646                            with always update last candle!
2647        :param csvSep: separator if csv-file is used, `,` by default.
2648        :param show: if `True` then also prints Pandas DataFrame to the console.
2649        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2650                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2651        """
2652        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2653        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2654        history = None  # empty pandas object for history
2655
2656        if interval not in TKS_CANDLE_INTERVALS.keys():
2657            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2658            raise Exception("Incorrect value")
2659
2660        if not (self.ticker or self.figi):
2661            uLogger.error("Ticker or FIGI must be defined!")
2662            raise Exception("Ticker or FIGI required")
2663
2664        if self.ticker and not self.figi:
2665            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2666            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2667
2668        if self.figi and not self.ticker:
2669            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2670            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2671
2672        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2673        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2674        if interval.lower() != "day":
2675            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2676
2677        delta = dtEnd - dtStart  # current UTC time minus last time in file
2678        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2679
2680        # calculate history length in candles:
2681        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2682        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2683            length += 1  # to avoid fraction time
2684
2685        # calculate data blocks count:
2686        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2687
2688        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2689        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2690        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2691        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2692        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2693
2694        tempOld = None  # pandas object for old history, if --only-missing key present
2695        lastTime = None  # datetime object of last old candle in file
2696
2697        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2698            uLogger.debug("--only-missing key present, add only last missing candles...")
2699            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2700
2701            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2702
2703            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2704            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2705            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2706            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2707
2708            # get last datetime object from last string in file or minus 1 delta if file is empty:
2709            if len(tempOld) > 0:
2710                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2711
2712            else:
2713                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2714
2715            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2716
2717        responseJSONs = []  # raw history blocks of data
2718
2719        blockEnd = dtEnd
2720        for item in range(blocks):
2721            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2722            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2723
2724            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2725                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2726            ))
2727
2728            if blockStart == blockEnd:
2729                uLogger.debug("Skipped this zero-length block...")
2730
2731            else:
2732                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2733                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2734                self.body = str({
2735                    "figi": self.figi,
2736                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2737                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2738                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2739                })
2740                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2741
2742                if "code" in responseJSON.keys():
2743                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2744
2745                else:
2746                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2747                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2748
2749                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2750
2751            blockEnd = blockStart
2752
2753        printCount = len(responseJSONs)  # candles to show in console
2754        if responseJSONs:
2755            tempHistory = pd.DataFrame(
2756                data={
2757                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2758                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2759                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2760                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2761                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2762                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2763                    "volume": [int(item["volume"]) for item in responseJSONs],
2764                },
2765                index=range(len(responseJSONs)),
2766                columns=["date", "time", "open", "high", "low", "close", "volume"],
2767            )
2768            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2769            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2770
2771            # append only newest candles to old history if --only-missing key present:
2772            if onlyMissing and tempOld is not None and lastTime is not None:
2773                index = 0  # find start index in tempHistory data:
2774
2775                for i, item in tempHistory.iterrows():
2776                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2777
2778                    if curTime == lastTime:
2779                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2780                        index = i
2781                        printCount = index + 1
2782                        break
2783
2784                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2785
2786            else:
2787                history = tempHistory  # if no `--only-missing` key then load full data from server
2788
2789            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2790
2791        if history is not None and not history.empty:
2792            if show:
2793                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2794                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2795                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2796                ))
2797
2798        else:
2799            uLogger.warning("Received an empty candles history!")
2800
2801        if self.historyFile is not None:
2802            if history is not None and not history.empty:
2803                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2804                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2805
2806            else:
2807                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2808
2809        else:
2810            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2811
2812        return history
2813
2814    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2815        """
2816        Load candles history from csv-file and return Pandas DataFrame object.
2817
2818        See also: `History()` and `ShowHistoryChart()` methods.
2819
2820        :param filePath: path to csv-file to open.
2821        """
2822        loadedHistory = None  # init candles data object
2823
2824        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2825
2826        if os.path.exists(filePath):
2827            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2828
2829            tfStr = self.priceModel.FormattedDelta(
2830                self.priceModel.timeframe,
2831                "{days} days {hours}h {minutes}m {seconds}s",
2832            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2833                self.priceModel.timeframe,
2834                "{hours}h {minutes}m {seconds}s",
2835            )
2836
2837            if loadedHistory is not None and not loadedHistory.empty:
2838                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2839                    len(loadedHistory),
2840                    tfStr,
2841                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2842                )
2843
2844            else:
2845                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2846
2847        else:
2848            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2849
2850        return loadedHistory
2851
2852    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2853        """
2854        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2855
2856        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2857        Default: `index.html` (both for interact and non-interact candlesticks chart).
2858
2859        See also: `History()` and `LoadHistory()` methods.
2860
2861        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2862        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2863                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2864                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2865                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2866        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2867                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2868        """
2869        if isinstance(candles, str):
2870            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2871            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2872
2873        elif isinstance(candles, pd.DataFrame):
2874            self.priceModel.prices = candles  # set candles chain from variable
2875            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2876
2877            if "datetime" not in candles.columns:
2878                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2879
2880        else:
2881            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2882            raise Exception("Incorrect value")
2883
2884        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2885
2886        if interact:
2887            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2888
2889            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2890
2891        else:
2892            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2893
2894            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2895
2896        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2897
2898    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2899        """
2900        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2901        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2902
2903        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2904
2905        :param operation: string "Buy" or "Sell".
2906        :param lots: volume, integer count of lots >= 1.
2907        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2908        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2909        :param expDate: string "Undefined" by default or local date in future,
2910                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2911        :return: JSON with response from broker server.
2912        """
2913        if self.accountId is None or not self.accountId:
2914            uLogger.error("Variable `accountId` must be defined for using this method!")
2915            raise Exception("Account ID required")
2916
2917        if operation is None or not operation or operation not in ("Buy", "Sell"):
2918            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2919            raise Exception("Incorrect value")
2920
2921        if lots is None or lots < 1:
2922            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2923            lots = 1
2924
2925        if tp is None or tp < 0:
2926            tp = 0
2927
2928        if sl is None or sl < 0:
2929            sl = 0
2930
2931        if expDate is None or not expDate:
2932            expDate = "Undefined"
2933
2934        if not (self.ticker or self.figi):
2935            uLogger.error("Ticker or FIGI must be defined!")
2936            raise Exception("Ticker or FIGI required")
2937
2938        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2939        self.ticker = instrument["ticker"]
2940        self.figi = instrument["figi"]
2941
2942        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2943
2944        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2945        self.body = str({
2946            "figi": self.figi,
2947            "quantity": str(lots),
2948            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2949            "accountId": str(self.accountId),
2950            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2951        })
2952        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2953
2954        if "orderId" in response.keys():
2955            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2956                operation, response["orderId"],
2957                self.ticker, self.figi, lots,
2958                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2959                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2960                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2961            ))
2962
2963        else:
2964            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2965
2966        if tp > 0:
2967            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2968
2969        if sl > 0:
2970            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2971
2972        return response
2973
2974    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2975        """
2976        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2977        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2978
2979        See also: `Order()` and `Trade()` docstrings.
2980
2981        :param lots: volume, integer count of lots >= 1.
2982        :param tp: float > 0, take profit price of stop-order.
2983        :param sl: float > 0, stop loss price of stop-order.
2984        :param expDate: it's a local date in future.
2985                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2986        :return: JSON with response from broker server.
2987        """
2988        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2989
2990    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2991        """
2992        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2993        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2994
2995        See also: `Order()` and `Trade()` docstrings.
2996
2997        :param lots: volume, integer count of lots >= 1.
2998        :param tp: float > 0, take profit price of stop-order.
2999        :param sl: float > 0, stop loss price of stop-order.
3000        :param expDate: it's a local date in the future.
3001                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3002        :return: JSON with response from broker server.
3003        """
3004        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3005
3006    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3007        """
3008        Close position of given instruments.
3009
3010        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3011        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3012                         This avoids unnecessary downloading data from the server.
3013        """
3014        if instruments is None or not instruments:
3015            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3016            raise Exception("Ticker or FIGI required")
3017
3018        if isinstance(instruments, str):
3019            instruments = [instruments]
3020
3021        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3022        if uniqueInstruments:
3023            if portfolio is None or not portfolio:
3024                portfolio = self.Overview(show=False)
3025
3026            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3027            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3028
3029            for self.figi in uniqueInstruments:
3030                if self.figi not in allOpened:
3031                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3032                    continue
3033
3034                # search open trade info about instrument by ticker:
3035                instrument = {}
3036                for iType in TKS_INSTRUMENTS:
3037                    if instrument:
3038                        break
3039
3040                    for item in portfolio["stat"][iType]:
3041                        if item["figi"] == self.figi:
3042                            instrument = item
3043                            break
3044
3045                if instrument:
3046                    self.ticker = instrument["ticker"]
3047                    self.figi = instrument["figi"]
3048
3049                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3050                        self.ticker,
3051                        self.figi,
3052                        int(instrument["volume"]),
3053                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3054                    ))
3055
3056                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3057
3058                    if tradeLots > 0:
3059                        if instrument["blocked"] > 0:
3060                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3061                                instrument["blocked"],
3062                                self.ticker,
3063                                tradeLots,
3064                            ))
3065
3066                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3067                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3068
3069                    else:
3070                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3071
3072    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3073        """
3074        Close all positions of given instruments with defined type.
3075
3076        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3077        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3078                         This avoids unnecessary downloading data from the server.
3079        """
3080        if iType not in TKS_INSTRUMENTS:
3081            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3082
3083        else:
3084            if portfolio is None or not portfolio:
3085                portfolio = self.Overview(show=False)
3086
3087            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3088            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3089
3090            if tickers and portfolio:
3091                self.CloseTrades(tickers, portfolio)
3092
3093            else:
3094                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3095
3096    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3097        """
3098        Universal method to create market or limit orders with all available parameters for current `accountId`.
3099        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3100
3101        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3102        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3103
3104        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3105        then broker immediately open market order as you can do simple --buy or --sell operations!
3106
3107        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3108        When current price will go up or down to target price value then broker opens a limit order.
3109        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3110
3111        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3112
3113        :param operation: string "Buy" or "Sell".
3114        :param orderType: string "Limit" or "Stop".
3115        :param lots: volume, integer count of lots >= 1.
3116        :param targetPrice: target price > 0. This is open trade price for limit order.
3117        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3118                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3119        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3120                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3121                         Stop loss order always executed by market price.
3122        :param expDate: string "Undefined" by default or local date in future.
3123                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3124                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3125                        A limit order has no expiration date, it lasts until the end of the trading day.
3126        :return: JSON with response from broker server.
3127        """
3128        if self.accountId is None or not self.accountId:
3129            uLogger.error("Variable `accountId` must be defined for using this method!")
3130            raise Exception("Account ID required")
3131
3132        if operation is None or not operation or operation not in ("Buy", "Sell"):
3133            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3134            raise Exception("Incorrect value")
3135
3136        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3137            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3138            raise Exception("Incorrect value")
3139
3140        if lots is None or lots < 1:
3141            uLogger.error("You must define trade volume > 0: integer count of lots!")
3142            raise Exception("Incorrect value")
3143
3144        if targetPrice is None or targetPrice <= 0:
3145            uLogger.error("Target price for limit-order must be greater than 0!")
3146            raise Exception("Incorrect value")
3147
3148        if limitPrice is None or limitPrice <= 0:
3149            limitPrice = targetPrice
3150
3151        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3152            stopType = "Limit"
3153
3154        if expDate is None or not expDate:
3155            expDate = "Undefined"
3156
3157        if not (self.ticker or self.figi):
3158            uLogger.error("Tocker or FIGI must be defined!")
3159            raise Exception("Ticker or FIGI required")
3160
3161        response = {}
3162        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3163        self.ticker = instrument["ticker"]
3164        self.figi = instrument["figi"]
3165
3166        if orderType == "Limit":
3167            uLogger.debug(
3168                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3169                    self.ticker, self.figi,
3170                    operation, lots, targetPrice, instrument["currency"],
3171                ))
3172
3173            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3174            self.body = str({
3175                "figi": self.figi,
3176                "quantity": str(lots),
3177                "price": FloatToNano(targetPrice),
3178                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3179                "accountId": str(self.accountId),
3180                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3181            })
3182            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3183
3184            if "orderId" in response.keys():
3185                uLogger.info(
3186                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3187                        response["orderId"],
3188                        self.ticker, self.figi,
3189                        operation, lots, targetPrice, instrument["currency"],
3190                    ))
3191
3192                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3193                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3194                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3195                            targetPrice, instrument["currency"],
3196                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3197                        ))
3198
3199                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3200                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3201                            targetPrice, instrument["currency"],
3202                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3203                        ))
3204
3205            else:
3206                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3207
3208        if orderType == "Stop":
3209            uLogger.debug(
3210                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3211                    self.ticker, self.figi,
3212                    operation, lots,
3213                    targetPrice, instrument["currency"],
3214                    limitPrice, instrument["currency"],
3215                    stopType, expDate,
3216                ))
3217
3218            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3219            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3220            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3221
3222            body = {
3223                "figi": self.figi,
3224                "quantity": str(lots),
3225                "price": FloatToNano(limitPrice),
3226                "stopPrice": FloatToNano(targetPrice),
3227                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3228                "accountId": str(self.accountId),
3229                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3230                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3231            }
3232
3233            if expDateUTC:
3234                body["expireDate"] = expDateUTC
3235
3236            self.body = str(body)
3237            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3238
3239            if "stopOrderId" in response.keys():
3240                uLogger.info(
3241                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3242                        response["stopOrderId"],
3243                        self.ticker, self.figi,
3244                        operation, lots,
3245                        targetPrice, instrument["currency"],
3246                        limitPrice, instrument["currency"],
3247                        TKS_STOP_ORDER_TYPES[stopOrderType],
3248                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3249                    ))
3250
3251                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3252                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3253                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3254                            targetPrice, instrument["currency"],
3255                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3256                        ))
3257
3258                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3259                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3260                            targetPrice, instrument["currency"],
3261                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3262                        ))
3263
3264            else:
3265                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3266
3267        return response
3268
3269    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3270        """
3271        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3272        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3273        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3274        See also: `Order()` docstring.
3275
3276        :param lots: volume, integer count of lots >= 1.
3277        :param targetPrice: target price > 0. This is open trade price for limit order.
3278        :return: JSON with response from broker server.
3279        """
3280        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3281
3282    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3283        """
3284        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3285        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3286        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3287        target price value then broker opens a limit order. See also: `Order()` docstring.
3288
3289        :param lots: volume, integer count of lots >= 1.
3290        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3291        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3292                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3293        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3294                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3295        :param expDate: string "Undefined" by default or local date in future.
3296                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3297                        This date is converting to UTC format for server.
3298        :return: JSON with response from broker server.
3299        """
3300        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3301
3302    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3303        """
3304        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3305        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3306        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3307        See also: `Order()` docstring.
3308
3309        :param lots: volume, integer count of lots >= 1.
3310        :param targetPrice: target price > 0. This is open trade price for limit order.
3311        :return: JSON with response from broker server.
3312        """
3313        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3314
3315    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3316        """
3317        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3318        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3319        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3320        target price value then broker opens a limit order. See also: `Order()` docstring.
3321
3322        :param lots: volume, integer count of lots >= 1.
3323        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3324        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3325                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3326        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3327                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3328        :param expDate: string "Undefined" by default or local date in future.
3329                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3330                        This date is converting to UTC format for server.
3331        :return: JSON with response from broker server.
3332        """
3333        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3334
3335    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3336        """
3337        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3338
3339        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3340        :param allOrdersIDs: pre-received lists of all active pending orders.
3341                             This avoids unnecessary downloading data from the server.
3342        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3343        """
3344        if self.accountId is None or not self.accountId:
3345            uLogger.error("Variable `accountId` must be defined for using this method!")
3346            raise Exception("Account ID required")
3347
3348        if orderIDs:
3349            if allOrdersIDs is None or not allOrdersIDs:
3350                rawOrders = self.RequestPendingOrders()
3351                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3352
3353            if allStopOrdersIDs is None or not allStopOrdersIDs:
3354                rawStopOrders = self.RequestStopOrders()
3355                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3356
3357            for orderID in orderIDs:
3358                idInPendingOrders = orderID in allOrdersIDs
3359                idInStopOrders = orderID in allStopOrdersIDs
3360
3361                if not (idInPendingOrders or idInStopOrders):
3362                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3363                    continue
3364
3365                else:
3366                    if idInPendingOrders:
3367                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3368
3369                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3370                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3371                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3372                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3373
3374                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3375                            if self.moreDebug:
3376                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3377
3378                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3379
3380                        else:
3381                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3382
3383                    elif idInStopOrders:
3384                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3385
3386                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3387                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3388                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3389                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3390
3391                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3392                            if self.moreDebug:
3393                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3394
3395                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3396
3397                        else:
3398                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3399
3400                    else:
3401                        continue
3402
3403    def CloseAllOrders(self) -> None:
3404        """
3405        Gets a list of open pending and stop orders and cancel it all.
3406        """
3407        rawOrders = self.RequestPendingOrders()
3408        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3409        lenOrders = len(allOrdersIDs)
3410
3411        rawStopOrders = self.RequestStopOrders()
3412        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3413        lenSOrders = len(allStopOrdersIDs)
3414
3415        if lenOrders > 0 or lenSOrders > 0:
3416            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3417
3418            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3419
3420        else:
3421            uLogger.info("Orders not found, nothing to cancel.")
3422
3423    def CloseAll(self, *args) -> None:
3424        """
3425        Close all available (not blocked) opened trades and orders.
3426
3427        Also, you can select one or more keywords case-insensitive:
3428        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3429
3430        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3431        """
3432        overview = self.Overview(show=False)  # get all open trades info
3433
3434        if len(args) == 0:
3435            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3436            self.CloseAllOrders()  # close all pending and stop orders
3437
3438            for iType in TKS_INSTRUMENTS:
3439                if iType != "Currencies":
3440                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3441
3442        else:
3443            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3444            lowerArgs = [x.lower() for x in args]
3445
3446            if "orders" in lowerArgs:
3447                self.CloseAllOrders()  # close all pending and stop orders
3448
3449            for iType in TKS_INSTRUMENTS:
3450                if iType.lower() in lowerArgs and iType != "Currencies":
3451                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3452
3453    @staticmethod
3454    def ParseOrderParameters(operation, **inputParameters):
3455        """
3456        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3457
3458        :param operation: string "Buy" or "Sell".
3459        :param inputParameters: this is dict of strings that looks like this
3460               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3461               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3462               "prices" key: one or more prices to open limit-orders
3463               Counts of values in lots and prices lists must be equals!
3464        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3465        """
3466        # TODO: update order grid work with api v2
3467        pass
3468        # uLogger.debug("Input parameters: {}".format(inputParameters))
3469        #
3470        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3471        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3472        #     raise Exception("Incorrect value")
3473        #
3474        # if "l" in inputParameters.keys():
3475        #     inputParameters["lots"] = inputParameters.pop("l")
3476        #
3477        # if "p" in inputParameters.keys():
3478        #     inputParameters["prices"] = inputParameters.pop("p")
3479        #
3480        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3481        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3482        #     raise Exception("Incorrect value")
3483        #
3484        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3485        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3486        #
3487        # if len(lots) != len(prices):
3488        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3489        #     raise Exception("Incorrect value")
3490        #
3491        # uLogger.debug("Extracted parameters for orders:")
3492        # uLogger.debug("lots = {}".format(lots))
3493        # uLogger.debug("prices = {}".format(prices))
3494        #
3495        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3496        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3497        # uLogger.debug("Order parameters: {}".format(result))
3498        #
3499        # return result
3500
3501    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3502        """
3503        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3504
3505        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3506        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3507        """
3508        result = False
3509        msg = "Instrument not defined!"
3510
3511        if portfolio is None or not portfolio:
3512            portfolio = self.Overview(show=False)
3513
3514        if self.ticker:
3515            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3516            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3517
3518            for iType in TKS_INSTRUMENTS:
3519                for instrument in portfolio["stat"][iType]:
3520                    if instrument["ticker"] == self.ticker:
3521                        result = True
3522                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3523                        break
3524
3525        elif self.figi:
3526            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3527            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3528
3529            for iType in TKS_INSTRUMENTS:
3530                for instrument in portfolio["stat"][iType]:
3531                    if instrument["figi"] == self.figi:
3532                        result = True
3533                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3534                        break
3535
3536        else:
3537            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3538
3539        uLogger.debug(msg)
3540
3541        return result
3542
3543    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3544        """
3545        Returns instrument is in the user's portfolio if it presents there.
3546        Instrument must be defined by `ticker` (highly priority) or `figi`.
3547
3548        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3549        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3550        """
3551        result = None
3552        msg = "Instrument not defined!"
3553
3554        if portfolio is None or not portfolio:
3555            portfolio = self.Overview(show=False)
3556
3557        if self.ticker:
3558            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3559            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3560
3561            for iType in TKS_INSTRUMENTS:
3562                for instrument in portfolio["stat"][iType]:
3563                    if instrument["ticker"] == self.ticker:
3564                        result = instrument
3565                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3566                        break
3567
3568        elif self.figi:
3569            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3570            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3571
3572            for iType in TKS_INSTRUMENTS:
3573                for instrument in portfolio["stat"][iType]:
3574                    if instrument["figi"] == self.figi:
3575                        result = instrument
3576                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3577                        break
3578
3579        else:
3580            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3581
3582        uLogger.debug(msg)
3583
3584        return result
3585
3586    def RequestLimits(self) -> dict:
3587        """
3588        Method for obtaining the available funds for withdrawal for current `accountId`.
3589
3590        See also:
3591        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3592        - `OverviewLimits()` method
3593
3594        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3595                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3596                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3597                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3598        """
3599        if self.accountId is None or not self.accountId:
3600            uLogger.error("Variable `accountId` must be defined for using this method!")
3601            raise Exception("Account ID required")
3602
3603        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3604
3605        self.body = str({"accountId": self.accountId})
3606        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3607        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3608
3609        if self.moreDebug:
3610            uLogger.debug("Records about available funds for withdrawal successfully received")
3611
3612        return rawLimits
3613
3614    def OverviewLimits(self, show: bool = False) -> dict:
3615        """
3616        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3617
3618        See also: `RequestLimits()`.
3619
3620        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3621        :return: dict with raw parsed data from server and some calculated statistics about it.
3622        """
3623        if self.accountId is None or not self.accountId:
3624            uLogger.error("Variable `accountId` must be defined for using this method!")
3625            raise Exception("Account ID required")
3626
3627        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3628
3629        view = {
3630            "rawLimits": rawLimits,
3631            "limits": {  # parsed data for every currency:
3632                "money": {  # this is an array of portfolio currency positions
3633                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3634                },
3635                "blocked": {  # this is an array of blocked currency
3636                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3637                },
3638                "blockedGuarantee": {  # this is locked money under collateral for futures
3639                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3640                },
3641            },
3642        }
3643
3644        # --- Prepare text table with limits in human-readable format:
3645        if show:
3646            info = [
3647                "# Withdrawal limits\n\n",
3648                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3649                "* **Account ID:** [{}]\n".format(self.accountId),
3650            ]
3651
3652            if view["limits"]["money"]:
3653                info.extend([
3654                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3655                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3656                ])
3657
3658            else:
3659                info.append("\nNo withdrawal limits\n")
3660
3661            for curr in view["limits"]["money"].keys():
3662                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3663                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3664                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3665
3666                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3667                    "[{}]".format(curr),
3668                    "{:.2f}".format(view["limits"]["money"][curr]),
3669                    "{:.2f}".format(availableMoney),
3670                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3671                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3672                )
3673
3674                if curr == "rub":
3675                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3676
3677                else:
3678                    info.append(infoStr)
3679
3680            infoText = "".join(info)
3681
3682            uLogger.info(infoText)
3683
3684            if self.withdrawalLimitsFile:
3685                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3686                    fH.write(infoText)
3687
3688                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3689
3690        return view
3691
3692    def RequestAccounts(self) -> dict:
3693        """
3694        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3695
3696        See also:
3697        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3698        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3699        - `OverviewUserInfo()` method
3700
3701        :return: dict with raw data from server that contains accounts info. Example of dict:
3702                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3703                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3704                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3705                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3706        """
3707        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3708
3709        self.body = str({})
3710        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3711        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3712
3713        if self.moreDebug:
3714            uLogger.debug("Records about available accounts successfully received")
3715
3716        return rawAccounts
3717
3718    def RequestUserInfo(self) -> dict:
3719        """
3720        Method for requesting common user's information.
3721
3722        See also:
3723        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3724        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3725        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3726        - `OverviewUserInfo()` method
3727
3728        :return: dict with raw data from server that contains user's information. Example of dict:
3729                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3730                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3731        """
3732        uLogger.debug("Requesting common user's information. Wait, please...")
3733
3734        self.body = str({})
3735        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3736        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3737
3738        if self.moreDebug:
3739            uLogger.debug("Records about current user successfully received")
3740
3741        return rawUserInfo
3742
3743    def RequestMarginStatus(self, accountId: str = None) -> dict:
3744        """
3745        Method for requesting margin calculation for defined account ID.
3746
3747        See also:
3748        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3749        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3750        - `OverviewUserInfo()` method
3751
3752        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3753        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3754                 Example of responses:
3755                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3756                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3757                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3758                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3759                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3760                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3761        """
3762        if accountId is None or not accountId:
3763            if self.accountId is None or not self.accountId:
3764                uLogger.error("Variable `accountId` must be defined for using this method!")
3765                raise Exception("Account ID required")
3766
3767            else:
3768                accountId = self.accountId  # use `self.accountId` (main ID) by default
3769
3770        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3771
3772        self.body = str({"accountId": accountId})
3773        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3774        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3775
3776        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3777            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3778            rawMargin = {}
3779
3780        else:
3781            if self.moreDebug:
3782                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3783
3784        return rawMargin
3785
3786    def RequestTariffLimits(self) -> dict:
3787        """
3788        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3789
3790        See also:
3791        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3792        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3793        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3794        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3795        - `OverviewUserInfo()` method
3796
3797        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3798                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3799                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3800        """
3801        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3802
3803        self.body = str({})
3804        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3805        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3806
3807        if self.moreDebug:
3808            uLogger.debug("Records with limits of current tariff successfully received")
3809
3810        return rawTariffLimits
3811
3812    def RequestBondCoupons(self, iJSON: dict) -> dict:
3813        """
3814        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3815        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3816        All dates are in UTC timezone.
3817
3818        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3819        Documentation:
3820        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3821        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3822
3823        See also: `ExtendBondsData()`.
3824
3825        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3826                      If raw iJSON is not data of bond then server returns an error [400] with message:
3827                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3828        :return: dictionary with bond payment calendar. Response example
3829                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3830                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3831                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3832                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3833        """
3834        if iJSON["figi"] is None or not iJSON["figi"]:
3835            uLogger.error("FIGI must be defined for using this method!")
3836            raise Exception("FIGI required")
3837
3838        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3839        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3840
3841        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3842            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3843            self.figi,
3844            startDate,
3845            endDate,
3846        ))
3847
3848        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3849        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3850        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3851
3852        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3853            uLogger.warning("Instrument type is not bond!")
3854
3855        else:
3856            if self.moreDebug:
3857                uLogger.debug("Records about bond payment calendar successfully received")
3858
3859        return calendar
3860
3861    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3862        """
3863        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3864        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3865        coupon yields, current yields and some statistics etc.
3866
3867        WARNING! This is too long operation if a lot of bonds requested from broker server.
3868
3869        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3870
3871        :param instruments: list of strings with tickers or FIGIs.
3872        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3873                     for further used by data scientists or stock analytics.
3874        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3875                 In XLSX-file and Pandas DataFrame fields mean:
3876                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3877                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3878        """
3879        if instruments is None or not instruments:
3880            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3881            raise Exception("Ticker or FIGI required")
3882
3883        if isinstance(instruments, str):
3884            instruments = [instruments]
3885
3886        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3887
3888        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3889
3890        iCount = len(uniqueInstruments)
3891        tooLong = iCount >= 20
3892        if tooLong:
3893            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3894
3895        bonds = None
3896        for i, self.figi in enumerate(uniqueInstruments):
3897            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3898
3899            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3900                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3901                rawBond = self.SearchByFIGI(requestPrice=True)
3902
3903                # Widen raw data with UTC current time (iData["actualDateTime"]):
3904                actualDate = datetime.now(tzutc())
3905                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3906
3907                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3908                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3909
3910                # Replace some values with human-readable:
3911                iData["nominalCurrency"] = iData["nominal"]["currency"]
3912                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3913                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3914                iData["aciCurrency"] = iData["aciValue"]["currency"]
3915                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3916                iData["issueSize"] = int(iData["issueSize"])
3917                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3918                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3919                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3920                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3921                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3922                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3923                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3924                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3925                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3926                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3927
3928                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3929                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3930                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3931                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3932                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3933                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3934                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3935                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3936                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3937                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3938                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3939
3940                # Widen raw data with calendar data from `rawCalendar` values:
3941                calendarData = []
3942                if "events" in iData["rawCalendar"].keys():
3943                    for item in iData["rawCalendar"]["events"]:
3944                        calendarData.append({
3945                            "couponDate": item["couponDate"],
3946                            "couponNumber": int(item["couponNumber"]),
3947                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3948                            "payCurrency": item["payOneBond"]["currency"],
3949                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3950                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3951                            "couponStartDate": item["couponStartDate"],
3952                            "couponEndDate": item["couponEndDate"],
3953                            "couponPeriod": item["couponPeriod"],
3954                        })
3955
3956                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3957                    if "maturityDate" not in iData.keys():
3958                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3959
3960                # Widen raw data with Coupon Rate.
3961                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3962                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3963                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3964                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3965
3966                # Widen raw data with Yield to Maturity (YTM) on current date.
3967                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3968                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3969                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3970                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3971                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3972                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3973
3974                iData["calendar"] = calendarData  # adds calendar at the end
3975
3976                # Remove not used data:
3977                iData.pop("uid")
3978                iData.pop("positionUid")
3979                iData.pop("currentPrice")
3980                iData.pop("rawCalendar")
3981
3982                colNames = list(iData.keys())
3983                if bonds is None:
3984                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3985
3986                else:
3987                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3988
3989            else:
3990                uLogger.warning("Instrument is not a bond!")
3991
3992            processed = round(100 * (i + 1) / iCount, 1)
3993            if tooLong and processed % 5 == 0:
3994                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3995
3996            else:
3997                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3998
3999        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4000
4001        # Saving bonds from Pandas DataFrame to XLSX sheet:
4002        if xlsx and self.bondsXLSXFile:
4003            with pd.ExcelWriter(
4004                    path=self.bondsXLSXFile,
4005                    date_format=TKS_DATE_FORMAT,
4006                    datetime_format=TKS_DATE_TIME_FORMAT,
4007                    mode="w",
4008            ) as writer:
4009                bonds.to_excel(
4010                    writer,
4011                    sheet_name="Extended bonds data",
4012                    index=True,
4013                    encoding="UTF-8",
4014                    freeze_panes=(1, 1),
4015                )  # saving as XLSX-file with freeze first row and column as headers
4016
4017            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4018
4019        return bonds
4020
4021    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4022        """
4023        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4024
4025        WARNING! This is too long operation if a lot of bonds requested from broker server.
4026
4027        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4028
4029        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4030                        extended information about bonds: main info, current prices, bond payment calendar,
4031                        coupon yields, current yields and some statistics etc.
4032                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4033        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4034                     for further used by data scientists or stock analytics.
4035        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4036        """
4037        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4038            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4039
4040        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4041
4042        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4043        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4044        calendar = None
4045        for bond in extBonds.iterrows():
4046            for item in bond[1]["calendar"]:
4047                cData = {
4048                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4049                    "couponDate": item["couponDate"],
4050                    "figi": bond[1]["figi"],
4051                    "ticker": bond[1]["ticker"],
4052                    "name": bond[1]["name"],
4053                    "couponNumber": item["couponNumber"],
4054                    "payOneBond": item["payOneBond"],
4055                    "payCurrency": item["payCurrency"],
4056                    "couponType": item["couponType"],
4057                    "couponPeriod": item["couponPeriod"],
4058                    "fixDate": item["fixDate"],
4059                    "couponStartDate": item["couponStartDate"],
4060                    "couponEndDate": item["couponEndDate"],
4061                }
4062
4063                if calendar is None:
4064                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4065
4066                else:
4067                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4068
4069        if calendar is not None:
4070            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4071
4072            # Saving calendar from Pandas DataFrame to XLSX sheet:
4073            if xlsx:
4074                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4075
4076                with pd.ExcelWriter(
4077                        path=xlsxCalendarFile,
4078                        date_format=TKS_DATE_FORMAT,
4079                        datetime_format=TKS_DATE_TIME_FORMAT,
4080                        mode="w",
4081                ) as writer:
4082                    humanReadable = calendar.copy(deep=True)
4083                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4084                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4085                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4086                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4087                    humanReadable.columns = colNames  # human-readable column names
4088
4089                    humanReadable.to_excel(
4090                        writer,
4091                        sheet_name="Bond payments calendar",
4092                        index=False,
4093                        encoding="UTF-8",
4094                        freeze_panes=(1, 2),
4095                    )  # saving as XLSX-file with freeze first row and column as headers
4096
4097                    del humanReadable  # release df in memory
4098
4099                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4100
4101        return calendar
4102
4103    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4104        """
4105        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4106        Also, creates Markdown file with calendar data, `calendar.md` by default.
4107
4108        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4109
4110        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4111                        extended information about bonds: main info, current prices, bond payment calendar,
4112                        coupon yields, current yields and some statistics etc.
4113                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4114        :param show: if `True` then also printing bonds payment calendar to the console,
4115                     otherwise save to file `calendarFile` only. `False` by default.
4116        :return: multilines text in Markdown format with bonds payment calendar as a table.
4117        """
4118        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4119            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4120
4121        infoText = "# Bond payments calendar\n\n"
4122
4123        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4124
4125        if not (calendar is None or calendar.empty):
4126            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4127
4128            info = [
4129                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4130                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4131            ]
4132
4133            newMonth = False
4134            notOneBond = calendar["figi"].nunique() > 1
4135            for i, bond in enumerate(calendar.iterrows()):
4136                if newMonth and notOneBond:
4137                    info.append(splitLine)
4138
4139                info.append(
4140                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4141                        "  √" if bond[1]["paid"] else "  —",
4142                        bond[1]["couponDate"].split("T")[0],
4143                        bond[1]["figi"],
4144                        bond[1]["ticker"],
4145                        bond[1]["couponNumber"],
4146                        "{} {}".format(
4147                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4148                            bond[1]["payCurrency"],
4149                        ),
4150                        bond[1]["couponType"],
4151                        bond[1]["couponPeriod"],
4152                        bond[1]["fixDate"].split("T")[0],
4153                    )
4154                )
4155
4156                if i < len(calendar.values) - 1:
4157                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4158                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4159                    newMonth = False if curDate.month == nextDate.month else True
4160
4161                else:
4162                    newMonth = False
4163
4164            infoText += "".join(info)
4165
4166            if show:
4167                uLogger.info("{}".format(infoText))
4168
4169            if self.calendarFile is not None:
4170                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4171                    fH.write(infoText)
4172
4173                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4174
4175        else:
4176            infoText += "No data\n"
4177
4178        return infoText
4179
4180    def OverviewAccounts(self, show: bool = False) -> dict:
4181        """
4182        Method for parsing and show simple table with all available user accounts.
4183
4184        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4185
4186        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4187        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4188                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4189                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4190                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4191                                                        "closed": "—", "access": "Full access" }, ...}}`
4192        """
4193        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4194
4195        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4196        accounts = {
4197            item["id"]: {
4198                "type": TKS_ACCOUNT_TYPES[item["type"]],
4199                "name": item["name"],
4200                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4201                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4202                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4203                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4204            } for item in rawAccounts["accounts"]
4205        }
4206
4207        # Raw and parsed data with some fields replaced in "stat" section:
4208        view = {
4209            "rawAccounts": rawAccounts,
4210            "stat": accounts,
4211        }
4212
4213        # --- Prepare simple text table with only accounts data in human-readable format:
4214        if show:
4215            info = [
4216                "# User accounts\n\n",
4217                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4218                "| Account ID   | Type                      | Status                    | Name                           |\n",
4219                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4220            ]
4221
4222            for account in view["stat"].keys():
4223                info.extend([
4224                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4225                        account,
4226                        view["stat"][account]["type"],
4227                        view["stat"][account]["status"],
4228                        view["stat"][account]["name"],
4229                    )
4230                ])
4231
4232            infoText = "".join(info)
4233
4234            uLogger.info(infoText)
4235
4236            if self.userAccountsFile:
4237                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4238                    fH.write(infoText)
4239
4240                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4241
4242        return view
4243
4244    def OverviewUserInfo(self, show: bool = False) -> dict:
4245        """
4246        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4247
4248        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4249
4250        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4251        :return: dict with raw parsed data from server and some calculated statistics about it.
4252        """
4253        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4254        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4255        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4256        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4257        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4258        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4259
4260        # This is dict with parsed common user data:
4261        userInfo = {
4262            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4263            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4264            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4265            "tariff": rawUserInfo["tariff"],
4266        }
4267
4268        # This is an array of dict with parsed margin statuses for every account IDs:
4269        margins = {}
4270        for accountId in accounts.keys():
4271            if rawMargins[accountId]:
4272                margins[accountId] = {
4273                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4274                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4275                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4276                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4277                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4278                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4279                }
4280
4281            else:
4282                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4283
4284        unary = {}  # unary-connection limits
4285        for item in rawTariffLimits["unaryLimits"]:
4286            if item["limitPerMinute"] in unary.keys():
4287                unary[item["limitPerMinute"]].extend(item["methods"])
4288
4289            else:
4290                unary[item["limitPerMinute"]] = item["methods"]
4291
4292        stream = {}  # stream-connection limits
4293        for item in rawTariffLimits["streamLimits"]:
4294            if item["limit"] in stream.keys():
4295                stream[item["limit"]].extend(item["streams"])
4296
4297            else:
4298                stream[item["limit"]] = item["streams"]
4299
4300        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4301        limits = {
4302            "unary": unary,
4303            "stream": stream,
4304        }
4305
4306        # Raw and parsed data as an output result:
4307        view = {
4308            "rawUserInfo": rawUserInfo,
4309            "rawAccounts": rawAccounts,
4310            "rawMargins": rawMargins,
4311            "rawTariffLimits": rawTariffLimits,
4312            "stat": {
4313                "userInfo": userInfo,
4314                "accounts": accounts,
4315                "margins": margins,
4316                "limits": limits,
4317            },
4318        }
4319
4320        # --- Prepare text table with user information in human-readable format:
4321        if show:
4322            info = [
4323                "# Full user information\n\n",
4324                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4325                "## Common information\n\n",
4326                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4327                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4328                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4329                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4330                "\n## User accounts\n\n",
4331            ]
4332
4333            for account in view["stat"]["accounts"].keys():
4334                info.extend([
4335                    "### ID: [{}]\n\n".format(account),
4336                    "| Parameters           | Values                                                       |\n",
4337                    "|----------------------|--------------------------------------------------------------|\n",
4338                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4339                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4340                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4341                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4342                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4343                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4344                ])
4345
4346                if margins[account]:
4347                    info.extend([
4348                        "| Margin status:       | Enabled                                                      |\n",
4349                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4350                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4351                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4352                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4353                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4354                    ])
4355
4356                else:
4357                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4358
4359            info.extend([
4360                "\n## Current user tariff limits\n",
4361                "\nSee also:\n",
4362                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4363                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4364                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4365                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4366                "\n### Unary limits\n",
4367            ])
4368
4369            if unary:
4370                for key, values in sorted(unary.items()):
4371                    info.append("\n* Max requests per minute: {}\n".format(key))
4372
4373                    for value in values:
4374                        info.append("  - {}\n".format(value))
4375
4376            else:
4377                info.append("\nNot available\n")
4378
4379            info.append("\n### Stream limits\n")
4380
4381            if stream:
4382                for key, values in sorted(stream.items()):
4383                    info.append("\n* Max stream connections: {}\n".format(key))
4384
4385                    for value in values:
4386                        info.append("  - {}\n".format(value))
4387
4388            else:
4389                info.append("\nNot available\n")
4390
4391            infoText = "".join(info)
4392
4393            uLogger.info(infoText)
4394
4395            if self.userInfoFile:
4396                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4397                    fH.write(infoText)
4398
4399                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4400
4401        return view
4402
4403
4404class Args:
4405    """
4406    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4407    """
4408    def __init__(self, **kwargs):
4409        self.__dict__.update(kwargs)
4410
4411    def __getattr__(self, item):
4412        return None
4413
4414
4415def ParseArgs():
4416    """This function get and parse command line keys."""
4417    parser = ArgumentParser()  # command-line string parser
4418
4419    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4420    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4421
4422    # --- options:
4423
4424    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4425    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4426    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4427
4428    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4429    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4430
4431    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4432    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4433
4434    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4435
4436    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4437    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4438    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4439
4440    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4441    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4442
4443    # --- commands:
4444
4445    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4446
4447    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4448    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4449    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4450    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4451    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4452    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4453    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4454    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4455
4456    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4457    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4458    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4459    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4460    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4461
4462    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4463    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4464    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4465    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4466
4467    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4468    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4469    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4470
4471    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4472    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4473    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4474    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4475    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4476    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4477    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4478
4479    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4480    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4481    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4482    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4483    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4484
4485    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4486    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4487    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4488
4489    cmdArgs = parser.parse_args()
4490    return cmdArgs
4491
4492
4493def Main(**kwargs):
4494    """
4495    Main function for work with TKSBrokerAPI in the console.
4496
4497    See examples:
4498    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4499    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4500    """
4501    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4502
4503    if args.debug_level:
4504        uLogger.level = 10  # always debug level by default
4505        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4506
4507    exitCode = 0
4508    start = datetime.now(tzutc())
4509    uLogger.debug("=-" * 60)
4510    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4511        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4512        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4513    ))
4514
4515    # trying to calculate full current version:
4516    buildVersion = __version__
4517    try:
4518        v = version("tksbrokerapi")
4519        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4520
4521    except Exception:
4522        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4523
4524    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4525    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4526
4527    try:
4528        if args.version:
4529            print("TKSBrokerAPI {}".format(buildVersion))
4530            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4531
4532        else:
4533            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4534            server = TinkoffBrokerServer(
4535                token=args.token,
4536                accountId=args.account_id,
4537                useCache=not args.no_cache,
4538            )
4539
4540            # --- set some options:
4541
4542            if args.more:
4543                server.moreDebug = True
4544                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4545
4546            if args.ticker:
4547                if args.ticker in server.aliasesKeys:
4548                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4549
4550                else:
4551                    server.ticker = args.ticker
4552
4553            if args.figi:
4554                server.figi = args.figi
4555
4556            if args.depth is not None:
4557                server.depth = args.depth
4558
4559            # --- do one command:
4560
4561            if args.list:
4562                if args.output is not None:
4563                    server.instrumentsFile = args.output
4564
4565                server.ShowInstrumentsInfo(show=True)
4566
4567            elif args.list_xlsx:
4568                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4569
4570            elif args.bonds_xlsx is not None:
4571                if args.output is not None:
4572                    server.bondsXLSXFile = args.output
4573
4574                if len(args.bonds_xlsx) == 0:
4575                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4576
4577                else:
4578                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4579
4580            elif args.search:
4581                if args.output is not None:
4582                    server.searchResultsFile = args.output
4583
4584                server.SearchInstruments(pattern=args.search[0], show=True)
4585
4586            elif args.info:
4587                if not (args.ticker or args.figi):
4588                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4589                    raise Exception("Ticker or FIGI required")
4590
4591                if args.output is not None:
4592                    server.infoFile = args.output
4593
4594                if args.ticker:
4595                    server.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4596
4597                else:
4598                    server.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4599
4600            elif args.calendar is not None:
4601                if args.output is not None:
4602                    server.calendarFile = args.output
4603
4604                if len(args.calendar) == 0:
4605                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4606
4607                else:
4608                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4609
4610                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4611
4612            elif args.price:
4613                if not (args.ticker or args.figi):
4614                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4615                    raise Exception("Ticker or FIGI required")
4616
4617                server.GetCurrentPrices(show=True)
4618
4619            elif args.prices is not None:
4620                if args.output is not None:
4621                    server.pricesFile = args.output
4622
4623                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4624
4625            elif args.overview:
4626                if args.output is not None:
4627                    server.overviewFile = args.output
4628
4629                server.Overview(show=True, details="full")
4630
4631            elif args.overview_digest:
4632                if args.output is not None:
4633                    server.overviewDigestFile = args.output
4634
4635                server.Overview(show=True, details="digest")
4636
4637            elif args.overview_positions:
4638                if args.output is not None:
4639                    server.overviewPositionsFile = args.output
4640
4641                server.Overview(show=True, details="positions")
4642
4643            elif args.overview_orders:
4644                if args.output is not None:
4645                    server.overviewOrdersFile = args.output
4646
4647                server.Overview(show=True, details="orders")
4648
4649            elif args.overview_analytics:
4650                if args.output is not None:
4651                    server.overviewAnalyticsFile = args.output
4652
4653                server.Overview(show=True, details="analytics")
4654
4655            elif args.deals is not None:
4656                if args.output is not None:
4657                    server.reportFile = args.output
4658
4659                if 0 <= len(args.deals) < 3:
4660                    server.Deals(
4661                        start=args.deals[0] if len(args.deals) >= 1 else None,
4662                        end=args.deals[1] if len(args.deals) == 2 else None,
4663                        show=True,  # Always show deals report in console
4664                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4665                    )
4666
4667                else:
4668                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4669                    raise Exception("Incorrect value")
4670
4671            elif args.history is not None:
4672                if args.output is not None:
4673                    server.historyFile = args.output
4674
4675                if 0 <= len(args.history) < 3:
4676                    dataReceived = server.History(
4677                        start=args.history[0] if len(args.history) >= 1 else None,
4678                        end=args.history[1] if len(args.history) == 2 else None,
4679                        interval="hour" if args.interval is None or not args.interval else args.interval,
4680                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4681                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4682                        show=True,  # shows all downloaded candles in console
4683                    )
4684
4685                    if args.render_chart is not None and dataReceived is not None:
4686                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4687
4688                        server.ShowHistoryChart(
4689                            candles=dataReceived,
4690                            interact=iChart,
4691                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4692                        )
4693
4694                else:
4695                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4696                    raise Exception("Incorrect value")
4697
4698            elif args.load_history is not None:
4699                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4700
4701                if args.render_chart is not None and histData is not None:
4702                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4703                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4704
4705                    server.ShowHistoryChart(
4706                        candles=histData,
4707                        interact=iChart,
4708                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4709                    )
4710
4711            elif args.trade is not None:
4712                if 1 <= len(args.trade) <= 5:
4713                    server.Trade(
4714                        operation=args.trade[0],
4715                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4716                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4717                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4718                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4719                    )
4720
4721                else:
4722                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4723
4724            elif args.buy is not None:
4725                if 0 <= len(args.buy) <= 4:
4726                    server.Buy(
4727                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4728                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4729                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4730                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4731                    )
4732
4733                else:
4734                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4735
4736            elif args.sell is not None:
4737                if 0 <= len(args.sell) <= 4:
4738                    server.Sell(
4739                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4740                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4741                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4742                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4743                    )
4744
4745                else:
4746                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4747
4748            elif args.order:
4749                if 4 <= len(args.order) <= 7:
4750                    server.Order(
4751                        operation=args.order[0],
4752                        orderType=args.order[1],
4753                        lots=int(args.order[2]),
4754                        targetPrice=float(args.order[3]),
4755                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4756                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4757                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4758                    )
4759
4760                else:
4761                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4762
4763            elif args.buy_limit:
4764                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4765
4766            elif args.sell_limit:
4767                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4768
4769            elif args.buy_stop:
4770                if 2 <= len(args.buy_stop) <= 7:
4771                    server.BuyStop(
4772                        lots=int(args.buy_stop[0]),
4773                        targetPrice=float(args.buy_stop[1]),
4774                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4775                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4776                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4777                    )
4778
4779                else:
4780                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4781
4782            elif args.sell_stop:
4783                if 2 <= len(args.sell_stop) <= 7:
4784                    server.SellStop(
4785                        lots=int(args.sell_stop[0]),
4786                        targetPrice=float(args.sell_stop[1]),
4787                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4788                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4789                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4790                    )
4791
4792                else:
4793                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4794
4795            # elif args.buy_order_grid is not None:
4796            #     # update order grid work with api v2
4797            #     if len(args.buy_order_grid) == 2:
4798            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4799            #
4800            #         for order in orderParams:
4801            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4802            #
4803            #     else:
4804            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4805            #
4806            # elif args.sell_order_grid is not None:
4807            #     # update order grid work with api v2
4808            #     if len(args.sell_order_grid) >= 2:
4809            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4810            #
4811            #         for order in orderParams:
4812            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4813            #
4814            #     else:
4815            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4816
4817            elif args.close_order is not None:
4818                server.CloseOrders(args.close_order)  # close only one order
4819
4820            elif args.close_orders is not None:
4821                server.CloseOrders(args.close_orders)  # close list of orders
4822
4823            elif args.close_trade:
4824                if not (args.ticker or args.figi):
4825                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4826                    raise Exception("Ticker or FIGI required")
4827
4828                if args.ticker:
4829                    server.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4830
4831                else:
4832                    server.CloseTrades([args.figi])  # close only one trade by FIGI
4833
4834            elif args.close_trades is not None:
4835                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4836
4837            elif args.close_all is not None:
4838                server.CloseAll(*args.close_all)
4839
4840            elif args.limits:
4841                if args.output is not None:
4842                    server.withdrawalLimitsFile = args.output
4843
4844                server.OverviewLimits(show=True)
4845
4846            elif args.user_info:
4847                if args.output is not None:
4848                    server.userInfoFile = args.output
4849
4850                server.OverviewUserInfo(show=True)
4851
4852            elif args.account:
4853                if args.output is not None:
4854                    server.userAccountsFile = args.output
4855
4856                server.OverviewAccounts(show=True)
4857
4858            else:
4859                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4860                raise Exception("There is no command to execute")
4861
4862    except Exception:
4863        trace = tb.format_exc()
4864        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4865            if e in trace:
4866                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4867                break
4868
4869        uLogger.debug(trace)
4870        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4871        exitCode = 255  # an error occurred, must be open a ticket for this issue
4872
4873    finally:
4874        finish = datetime.now(tzutc())
4875
4876        if exitCode == 0:
4877            if args.more:
4878                uLogger.debug("All operations were finished success (summary code is 0).")
4879
4880        else:
4881            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4882                os.path.abspath(uLog.defaultLogFile), exitCode,
4883            ))
4884
4885        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4886        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4887            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4888            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4889        ))
4890        uLogger.debug("=-" * 60)
4891
4892        if not kwargs:
4893            sys.exit(exitCode)
4894
4895        else:
4896            return exitCode
4897
4898
4899if __name__ == "__main__":
4900    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 253
 254        See also: `SearchByTicker()`, `SearchInstruments()`.
 255        """
 256
 257        self.figi = ""
 258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 259
 260        See also: `SearchByFIGI()`, `SearchInstruments()`.
 261        """
 262
 263        self.depth = 1
 264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 265
 266        See also: `GetCurrentPrices()`.
 267        """
 268
 269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 271
 272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 273        """
 274
 275        uLogger.debug("Broker API server: {}".format(self.server))
 276
 277        self.timeout = 15
 278        """Server operations timeout in seconds. Default: `15`.
 279
 280        See also: `SendAPIRequest()`.
 281        """
 282
 283        self.headers = {
 284            "Content-Type": "application/json",
 285            "accept": "application/json",
 286            "Authorization": "Bearer {}".format(self.token),
 287            "x-app-name": "Tim55667757.TKSBrokerAPI",
 288        }
 289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 290
 291        See also: `SendAPIRequest()`.
 292        """
 293
 294        self.body = None
 295        """Request body which send to broker server. Default: `None`.
 296
 297        See also: `SendAPIRequest()`.
 298        """
 299
 300        self.moreDebug = False
 301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 302
 303        self.historyFile = None
 304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 305
 306        See also: `History()`.
 307        """
 308
 309        self.htmlHistoryFile = "index.html"
 310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 311
 312        See also: `ShowHistoryChart()`.
 313        """
 314
 315        self.instrumentsFile = "instruments.md"
 316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 317
 318        See also: `ShowInstrumentsInfo()`.
 319        """
 320
 321        self.searchResultsFile = "search-results.md"
 322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 323
 324        See also: `SearchInstruments()`.
 325        """
 326
 327        self.pricesFile = "prices.md"
 328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 329
 330        See also: `GetListOfPrices()`.
 331        """
 332
 333        self.infoFile = "info.md"
 334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 335
 336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 337        """
 338
 339        self.bondsXLSXFile = "ext-bonds.xlsx"
 340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 342
 343        See also: `ExtendBondsData()`.
 344        """
 345
 346        self.calendarFile = "calendar.md"
 347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 348        
 349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 350
 351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 352        """
 353
 354        self.overviewFile = "overview.md"
 355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 356
 357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 358        """
 359
 360        self.overviewDigestFile = "overview-digest.md"
 361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 362
 363        See also: `Overview()` with parameter `details="digest"`.
 364        """
 365
 366        self.overviewPositionsFile = "overview-positions.md"
 367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 368
 369        See also: `Overview()` with parameter `details="positions"`.
 370        """
 371
 372        self.overviewOrdersFile = "overview-orders.md"
 373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 374
 375        See also: `Overview()` with parameter `details="orders"`.
 376        """
 377
 378        self.overviewAnalyticsFile = "overview-analytics.md"
 379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 380
 381        See also: `Overview()` with parameter `details="analytics"`.
 382        """
 383
 384        self.reportFile = "deals.md"
 385        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 386
 387        See also: `Deals()`.
 388        """
 389
 390        self.withdrawalLimitsFile = "limits.md"
 391        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 392
 393        See also: `OverviewLimits()` and `RequestLimits()`.
 394        """
 395
 396        self.userInfoFile = "user-info.md"
 397        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 398
 399        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 400        """
 401
 402        self.userAccountsFile = "accounts.md"
 403        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 404
 405        See also: `OverviewAccounts()`, `RequestAccounts()`.
 406        """
 407
 408        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 409        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 410
 411        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 412
 413        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 414        """
 415
 416        self.iList = None  # init iList for raw instruments data
 417        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 418        
 419        See also: `Listing()`, `DumpInstruments()`.
 420        """
 421
 422        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 423        if useCache:
 424            if os.path.exists(self.iListDumpFile):
 425                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 426                curTime = datetime.now(tzutc())
 427
 428                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 429                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 430
 431                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 432
 433                else:
 434                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 435
 436                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 437                        os.path.abspath(self.iListDumpFile),
 438                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 439                    ))
 440
 441            else:
 442                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 443                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 444
 445        else:
 446            self.iList = self.Listing()  # request new raw instruments data from broker server
 447            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 448
 449        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 450        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 451
 452        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 453        """
 454
 455    def _ParseJSON(self, rawData="{}") -> dict:
 456        """
 457        Parse JSON from response string.
 458
 459        :param rawData: this is a string with JSON-formatted text.
 460        :return: JSON (dictionary), parsed from server response string.
 461        """
 462        responseJSON = json.loads(rawData) if rawData else {}
 463
 464        if self.moreDebug:
 465            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 466
 467        return responseJSON
 468
 469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 470        """
 471        Send GET or POST request to broker server and receive JSON object.
 472
 473        self.header: must be defining with dictionary of headers.
 474        self.body: if define then used as request body. None by default.
 475        self.timeout: global request timeout, 15 seconds by default.
 476        :param url: url with REST request.
 477        :param reqType: send "GET" or "POST" request. "GET" by default.
 478        :param retry: how many times retry after first request if an 5xx server errors occurred.
 479        :param pause: sleep time in seconds between retries.
 480        :return: response JSON (dictionary) from broker.
 481        """
 482        if reqType not in ("GET", "POST"):
 483            uLogger.error("You can define request type: 'GET' or 'POST'!")
 484            raise Exception("Incorrect value")
 485
 486        if self.moreDebug:
 487            uLogger.debug("Request parameters:")
 488            uLogger.debug("    - REST API URL: {}".format(url))
 489            uLogger.debug("    - request type: {}".format(reqType))
 490            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 491            uLogger.debug("    - body:\n{}".format(self.body))
 492
 493        # fast hack to avoid all operations with some tickers/FIGI
 494        responseJSON = {}
 495        oK = True
 496        for item in self.exclude:
 497            if item in url:
 498                if self.moreDebug:
 499                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 500
 501                oK = False
 502                break
 503
 504        if oK:
 505            counter = 0
 506            response = None
 507            errMsg = ""
 508
 509            while not response and counter <= retry:
 510                if reqType == "GET":
 511                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 512
 513                if reqType == "POST":
 514                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 515
 516                if self.moreDebug:
 517                    uLogger.debug("Response:")
 518                    uLogger.debug("    - status code: {}".format(response.status_code))
 519                    uLogger.debug("    - reason: {}".format(response.reason))
 520                    uLogger.debug("    - body length: {}".format(len(response.text)))
 521                    uLogger.debug("    - headers:\n{}".format(response.headers))
 522
 523                # Server returns some headers:
 524                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 525                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 526                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 527                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 528                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 529                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 530                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 531                    sleep(rateLimitWait)
 532
 533                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 534                if 400 <= response.status_code < 500:
 535                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 536                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 537                    counter = retry + 1
 538
 539                if 500 <= response.status_code < 600:
 540                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 541                    uLogger.debug("    - not oK, {}".format(errMsg))
 542                    counter += 1
 543
 544                    if counter <= retry:
 545                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 546                        sleep(pause)
 547
 548            responseJSON = self._ParseJSON(rawData=response.text)
 549
 550            if errMsg:
 551                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 552                uLogger.error("    - not oK, {}".format(errMsg))
 553
 554        return responseJSON
 555
 556    def _IUpdater(self, iType: str) -> tuple:
 557        """
 558        Request instrument by type from server. See available API methods for instruments:
 559        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 560        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 561        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 562        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 563        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 564
 565        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 566        :return: tuple with iType name and list of available instruments of current type for defined user token.
 567        """
 568        result = []
 569
 570        if iType in TKS_INSTRUMENTS:
 571            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 572
 573            # all instruments have the same body in API v2 requests:
 574            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 575            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 576            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 577
 578        return iType, result
 579
 580    def _IWrapper(self, kwargs):
 581        """
 582        Wrapper runs instrument's update method `_IUpdater()`.
 583        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 584        """
 585        return self._IUpdater(**kwargs)
 586
 587    def Listing(self) -> dict:
 588        """
 589        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 590
 591        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 592        """
 593        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 594        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 595
 596        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 597        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 598        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 599
 600        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 601        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 602        poolUpdater.close()
 603
 604        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 605        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 606        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 607
 608        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 609        for iType in iList.keys():
 610            for ticker in iList[iType]:
 611                iList[iType][ticker]["type"] = iType
 612
 613                if "minPriceIncrement" in iList[iType][ticker].keys():
 614                    iList[iType][ticker]["step"] = NanoToFloat(
 615                        iList[iType][ticker]["minPriceIncrement"]["units"],
 616                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 617                    )
 618
 619                else:
 620                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 621
 622        return iList
 623
 624    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 625        """
 626        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 627
 628        See also: `DumpInstruments()`, `Listing()`.
 629
 630        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 631                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 632        """
 633        if self.iListDumpFile is None or not self.iListDumpFile:
 634            uLogger.error("Output name of dump file must be defined!")
 635            raise Exception("Filename required")
 636
 637        if not self.iList or forceUpdate:
 638            self.iList = self.Listing()
 639
 640        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 641
 642        # Save as XLSX with separated sheets for every type of instruments:
 643        with pd.ExcelWriter(
 644                path=xlsxDumpFile,
 645                date_format=TKS_DATE_FORMAT,
 646                datetime_format=TKS_DATE_TIME_FORMAT,
 647                mode="w",
 648        ) as writer:
 649            for iType in TKS_INSTRUMENTS:
 650                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 651                df = df[sorted(df)]  # sorted by column names
 652                df = df.applymap(
 653                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 654                    na_action="ignore",
 655                )  # converting numbers from nano-type to float in every cell
 656                df.to_excel(
 657                    writer,
 658                    sheet_name=iType,
 659                    encoding="UTF-8",
 660                    freeze_panes=(1, 1),
 661                )  # saving as XLSX-file with freeze first row and column as headers
 662
 663        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 664
 665    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 666        """
 667        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 668        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 669
 670        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 671
 672        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 673                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 674        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 675        """
 676        if self.iListDumpFile is None or not self.iListDumpFile:
 677            uLogger.error("Output name of dump file must be defined!")
 678            raise Exception("Filename required")
 679
 680        if not self.iList or forceUpdate:
 681            self.iList = self.Listing()
 682
 683        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 684        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 685            fH.write(jsonDump)
 686
 687        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 688
 689        return jsonDump
 690
 691    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 692        """
 693        Show information about one instrument defined by json data and prints it in Markdown format.
 694
 695        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 696
 697        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 698        :param show: if `True` then also printing information about instrument and its current price.
 699        :return: multilines text in Markdown format with information about one instrument.
 700        """
 701        splitLine = "|                                                             |                                                        |\n"
 702        infoText = ""
 703
 704        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 705            info = [
 706                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 707                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 708                "| Parameters                                                  | Values                                                 |\n",
 709                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 710                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 711                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 712            ]
 713
 714            if "sector" in iJSON.keys() and iJSON["sector"]:
 715                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 716
 717            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 718                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 719                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 720            )))
 721
 722            info.extend([
 723                splitLine,
 724                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 725                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 726            ])
 727
 728            if "isin" in iJSON.keys() and iJSON["isin"]:
 729                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 730
 731            if "classCode" in iJSON.keys():
 732                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 733
 734            info.extend([
 735                splitLine,
 736                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 737                splitLine,
 738                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 739                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 740                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 741            ])
 742
 743            if iJSON["figi"]:
 744                self.figi = iJSON["figi"]
 745                iJSON = iJSON | self.RequestTradingStatus()
 746
 747                info.extend([
 748                    splitLine,
 749                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 750                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 751                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 752                ])
 753
 754            info.append(splitLine)
 755
 756            if "type" in iJSON.keys() and iJSON["type"]:
 757                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 758
 759            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 760                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 761
 762            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 763                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 766                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 769                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 770
 771            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 772                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 773
 774            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 775                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 776
 777            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 778                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 779
 780            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 781                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 782
 783            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 784                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 785
 786            if "currency" in iJSON.keys():
 787                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 788
 789            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 790                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 791
 792            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 793                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 794
 795            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 796                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 799                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 802                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 805                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 808                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 811                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 812
 813            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 814                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 815
 816            iExt = None
 817            if iJSON["type"] == "Bonds":
 818                info.extend([
 819                    splitLine,
 820                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 821                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 822                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 823                        iJSON["nominal"]["currency"],
 824                    )),
 825                ])
 826
 827                if "floatingCouponFlag" in iJSON.keys():
 828                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 829
 830                if "amortizationFlag" in iJSON.keys():
 831                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 832
 833                info.append(splitLine)
 834
 835                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 836                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 837
 838                if iJSON["figi"]:
 839                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 840
 841                    info.extend([
 842                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 843                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 844                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 845                    ])
 846
 847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 850                        iJSON["aciValue"]["currency"]
 851                    )))
 852
 853            if "currentPrice" in iJSON.keys():
 854                info.append(splitLine)
 855
 856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 858
 859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 864
 865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 867
 868                info.extend([
 869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 872                    )),
 873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 876                    )),
 877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 878                        "{:.2f}%{}".format(
 879                            iJSON["currentPrice"]["changes"],
 880                            " ({}{:.2f} {})".format(
 881                                "+" if bondChangesDelta > 0 else "",
 882                                bondChangesDelta,
 883                                aciCurrency
 884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 887                                currency
 888                            ),
 889                        )
 890                    ),
 891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 897                    )),
 898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 904                    )),
 905                ])
 906
 907            if "lot" in iJSON.keys():
 908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 909
 910            if "step" in iJSON.keys() and iJSON["step"] != 0:
 911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 912
 913            # Add bond payment calendar:
 914            if iJSON["type"] == "Bonds":
 915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 916                info.extend(["\n", strCalendar])
 917
 918            infoText += "".join(info)
 919
 920            if show:
 921                uLogger.info("{}".format(infoText))
 922
 923            else:
 924                uLogger.debug("{}".format(infoText))
 925
 926            if self.infoFile is not None:
 927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 928                    fH.write(infoText)
 929
 930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 931
 932        return infoText
 933
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 937
 938        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 939        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 940        :return: JSON formatted data with information about instrument.
 941        """
 942        tickerJSON = {}
 943        if self.moreDebug:
 944            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 945
 946        if not self.ticker:
 947            uLogger.warning("self.ticker variable is not be empty!")
 948
 949        else:
 950            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 951                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 952                raise Exception("Instrument not allowed")
 953
 954            if not self.iList:
 955                self.iList = self.Listing()
 956
 957            if self.ticker in self.iList["Shares"].keys():
 958                tickerJSON = self.iList["Shares"][self.ticker]
 959                if self.moreDebug:
 960                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 961
 962            elif self.ticker in self.iList["Currencies"].keys():
 963                tickerJSON = self.iList["Currencies"][self.ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Bonds"].keys():
 968                tickerJSON = self.iList["Bonds"][self.ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Etfs"].keys():
 973                tickerJSON = self.iList["Etfs"][self.ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Futures"].keys():
 978                tickerJSON = self.iList["Futures"][self.ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 981
 982        if tickerJSON:
 983            self.figi = tickerJSON["figi"]
 984
 985            if requestPrice:
 986                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 989                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    tickerJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1000
1001        return tickerJSON
1002
1003    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1004        """
1005        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1006
1007        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1008        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if self.moreDebug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if self.moreDebug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if self.moreDebug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if self.moreDebug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if self.moreDebug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON
1096
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1100        `{"buy": [{"price": 1243.8, "quantity": 193},
1101                  {"price": 1244.0, "quantity": 168},
1102                  {"price": 1244.8, "quantity": 5},
1103                  {"price": 1245.0, "quantity": 61},
1104                  {"price": 1245.4, "quantity": 60}],
1105          "sell": [{"price": 1243.6, "quantity": 8},
1106                   {"price": 1242.6, "quantity": 10},
1107                   {"price": 1242.4, "quantity": 18},
1108                   {"price": 1242.2, "quantity": 50},
1109                   {"price": 1242.0, "quantity": 113}],
1110          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1111        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1112        - sell: list of dicts with Buyers prices,
1113            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1114            - quantity: volume value by current price in lots,
1115        - limitUp: current trade session limit price, maximum,
1116        - limitDown: current trade session limit price, minimum,
1117        - lastPrice: last deal price of the instrument,
1118        - closePrice: previous trade session close price of the instrument.
1119
1120        See also: `SearchByTicker()` and `SearchByFIGI()`.
1121        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1122        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124        :param show: if `True` then print DOM to log and console.
1125        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1126                 If an error occurred then returns an empty record:
1127                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1128        """
1129        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1130
1131        if self.depth < 1:
1132            uLogger.error("Depth of Market (DOM) must be >=1!")
1133            raise Exception("Incorrect value")
1134
1135        if not (self.ticker or self.figi):
1136            uLogger.error("self.ticker or self.figi variables must be defined!")
1137            raise Exception("Ticker or FIGI required")
1138
1139        if self.ticker and not self.figi:
1140            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1141            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1142
1143        if not self.ticker and self.figi:
1144            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1145            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1146
1147        if not self.figi:
1148            uLogger.error("FIGI is not defined!")
1149            raise Exception("Ticker or FIGI required")
1150
1151        else:
1152            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1153
1154            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1155            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1156            self.body = str({"figi": self.figi, "depth": self.depth})
1157            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1158
1159            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1160                # list of dicts with sellers orders:
1161                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1162
1163                # list of dicts with buyers orders:
1164                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1165
1166                # max price of instrument at this time:
1167                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1168
1169                # min price of instrument at this time:
1170                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1171
1172                # last price of deal with instrument:
1173                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1174
1175                # last close price of instrument:
1176                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1177
1178            else:
1179                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1180                uLogger.debug("Server response: {}".format(pricesResponse))
1181
1182            if show:
1183                if prices["buy"] or prices["sell"]:
1184                    info = [
1185                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1186                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1187                            self.ticker,
1188                            self.figi,
1189                            self.depth,
1190                        ),
1191                        "-" * 60, "\n",
1192                        "             Orders of Buyers | Orders of Sellers\n",
1193                        "-" * 60, "\n",
1194                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1195                        "-" * 60, "\n",
1196                    ]
1197
1198                    if not prices["buy"]:
1199                        info.append("                              | No orders!\n")
1200                        sumBuy = 0
1201
1202                    else:
1203                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1204                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1205                        for item in maxMinSorted:
1206                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1207
1208                    if not prices["sell"]:
1209                        info.append("No orders!                    |\n")
1210                        sumSell = 0
1211
1212                    else:
1213                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1214                        for item in prices["sell"]:
1215                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1216
1217                    info.extend([
1218                        "-" * 60, "\n",
1219                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1220                        "-" * 60, "\n",
1221                    ])
1222
1223                    infoText = "".join(info)
1224
1225                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1226
1227                else:
1228                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1229
1230        return prices
1231
1232    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1233        """
1234        This method get and show information about all available broker instruments for current user account.
1235        If `instrumentsFile` string is not empty then also save information to this file.
1236
1237        :param show: if `True` then print results to console, if `False` - print only to file.
1238        :return: multi-lines string with all available broker instruments
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile:
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284        return infoText
1285
1286    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1287        """
1288        This method search and show information about instruments by part of its ticker, FIGI or name.
1289        If `searchResultsFile` string is not empty then also save information to this file.
1290
1291        :param pattern: string with part of ticker, FIGI or instrument's name.
1292        :param show: if `True` then print results to console, if `False` - return list of result only.
1293        :return: list of dictionaries with all found instruments.
1294        """
1295        if not self.iList:
1296            self.iList = self.Listing()
1297
1298        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1299        compiledPattern = re.compile(pattern, re.IGNORECASE)
1300
1301        for iType in self.iList:
1302            for instrument in self.iList[iType].values():
1303                searchResult = compiledPattern.search(" ".join(
1304                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1305                ))
1306
1307                if searchResult:
1308                    searchResults[iType][instrument["ticker"]] = instrument
1309
1310        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1311        info = [
1312            "# Search results\n\n",
1313            "* **Search pattern:** [{}]\n".format(pattern),
1314            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1315            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1316        ]
1317        infoShort = info[:]
1318
1319        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1320        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1321        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1322
1323        if resultsLen == 0:
1324            info.append("\nNo results\n")
1325            infoShort.append("\nNo results\n")
1326            uLogger.warning("No results. Try changing your search pattern.")
1327
1328        else:
1329            for iType in searchResults:
1330                iTypeValuesCount = len(searchResults[iType].values())
1331                if iTypeValuesCount > 0:
1332                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1334
1335                    for instrument in searchResults[iType].values():
1336                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1337                            instrument["type"],
1338                            instrument["ticker"],
1339                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1340                            instrument["figi"],
1341                        ))
1342
1343                    if iTypeValuesCount <= 5:
1344                        infoShort.extend(info[-iTypeValuesCount:])
1345
1346                    else:
1347                        infoShort.extend(info[-5:])
1348                        infoShort.append(skippedLine)
1349
1350        infoText = "".join(info)
1351        infoTextShort = "".join(infoShort)
1352
1353        if show:
1354            uLogger.info(infoTextShort)
1355            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1356
1357        if self.searchResultsFile:
1358            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1359                fH.write(infoText)
1360
1361            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1362
1363        return searchResults
1364
1365    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1366        """
1367        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1368
1369        :param instruments: list of strings with tickers or FIGIs.
1370        :return: list with unique instrument FIGIs only.
1371        """
1372        requestedInstruments = []
1373        for iName in instruments:
1374            if iName not in self.aliases.keys():
1375                if iName not in requestedInstruments:
1376                    requestedInstruments.append(iName)
1377
1378            else:
1379                if iName not in requestedInstruments:
1380                    if self.aliases[iName] not in requestedInstruments:
1381                        requestedInstruments.append(self.aliases[iName])
1382
1383        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1384
1385        onlyUniqueFIGIs = []
1386        for iName in requestedInstruments:
1387            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1388                continue
1389
1390            self.ticker = iName
1391            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1392
1393            if not iData:
1394                self.ticker = ""
1395                self.figi = iName
1396
1397                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1398
1399                if not iData:
1400                    self.figi = ""
1401                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1402
1403            if iData and iData["figi"] not in onlyUniqueFIGIs:
1404                onlyUniqueFIGIs.append(iData["figi"])
1405
1406        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1407
1408        return onlyUniqueFIGIs
1409
1410    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1411        """
1412        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1413        See limits: https://tinkoff.github.io/investAPI/limits/
1414        If `pricesFile` string is not empty then also save information to this file.
1415
1416        :param instruments: list of strings with tickers or FIGIs.
1417        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1418        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1420        """
1421        if instruments is None or not instruments:
1422            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1423            raise Exception("Ticker or FIGI required")
1424
1425        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1426
1427        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1428
1429        iList = []  # trying to get info and current prices about all unique instruments:
1430        for self.figi in onlyUniqueFIGIs:
1431            iData = self.SearchByFIGI(requestPrice=True)
1432            iList.append(iData)
1433
1434        self.ShowListOfPrices(iList, show)
1435
1436        return iList
1437
1438    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1439        """
1440        Show table contains current prices of given instruments.
1441
1442        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1443                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1444        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1445        :return: multilines text in Markdown format as a table contains current prices.
1446        """
1447        infoText = ""
1448
1449        if show or self.pricesFile:
1450            info = [
1451                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1452                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1453                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1454            ]
1455
1456            for item in iList:
1457                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1458                    item["ticker"],
1459                    item["figi"],
1460                    item["type"],
1461                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1462                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1463                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1464                    "{} / {}".format(
1465                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1466                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1467                    ),
1468                    "{} / {}".format(
1469                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1470                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1471                    ),
1472                    item["currency"],
1473                ))
1474
1475            infoText = "".join(info)
1476
1477            if show:
1478                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1479
1480            if self.pricesFile:
1481                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1482                    fH.write(infoText)
1483
1484                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1485
1486        return infoText
1487
1488    def RequestTradingStatus(self) -> dict:
1489        """
1490        Requesting trading status for the instrument defined by `figi` variable.
1491        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1492        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1493
1494        :return: dictionary with trading status attributes. Response example:
1495                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1496                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1497        """
1498        if self.figi is None or not self.figi:
1499            uLogger.error("Variable `figi` must be defined for using this method!")
1500            raise Exception("FIGI required")
1501
1502        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1503
1504        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1505        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1506        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1507
1508        if self.moreDebug:
1509            uLogger.debug("Records about current trading status successfully received")
1510
1511        return tradingStatus
1512
1513    def RequestPortfolio(self) -> dict:
1514        """
1515        Requesting actual user's portfolio for current `accountId`.
1516        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio
1535
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions
1558
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending orders for current `accountId`.
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1564
1565        :return: list of dictionaries with pending orders.
1566        """
1567        if self.accountId is None or not self.accountId:
1568            uLogger.error("Variable `accountId` must be defined for using this method!")
1569            raise Exception("Account ID required")
1570
1571        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1572
1573        self.body = str({"accountId": self.accountId})
1574        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1575        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1576
1577        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1578
1579        return rawOrders
1580
1581    def RequestStopOrders(self) -> list:
1582        """
1583        Requesting current actual stop orders for current `accountId`.
1584        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1585        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1586
1587        :return: list of dictionaries with stop orders.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1597        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1598
1599        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1600
1601        return rawStopOrders
1602
1603    def Overview(self, show: bool = False, details: str = "full") -> dict:
1604        """
1605        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1606        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1607        are defined then also save information to file.
1608
1609        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1610        many requests about the state of the portfolio, and then, based on the received data, a large number
1611        of calculation and statistics are collected.
1612
1613        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1614        :param details: how detailed should the information be? You should specify one of strings:
1615                        `full` - shows full available information about portfolio status (by default),
1616                        `positions` - shows only open positions,
1617                        `digest` - show a short digest of the portfolio status,
1618                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1619                        `orders` - shows only sections of open limits and stop orders.
1620        :return: dictionary with client's raw portfolio and some statistics.
1621        """
1622        if self.accountId is None or not self.accountId:
1623            uLogger.error("Variable `accountId` must be defined for using this method!")
1624            raise Exception("Account ID required")
1625
1626        view = {
1627            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1628                "headers": {},  # list of dictionaries, response headers without "positions" section
1629                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1630                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1631                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1632                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1633                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1634                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1635                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1636                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1637                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1638            },
1639            "stat": {  # --- some statistics calculated using "raw" sections:
1640                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1641                "availableRUB": 0.,  # available rubles (without other currencies)
1642                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1643                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1644                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1645                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1646                "sharesCostRUB": 0.,  # costs of all shares in RUB
1647                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1648                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1649                "futuresCostRUB": 0.,  # costs of all futures in RUB
1650                "Currencies": [],  # list of dictionaries of all currencies statistics
1651                "Shares": [],  # list of dictionaries of all shares statistics
1652                "Bonds": [],  # list of dictionaries of all bonds statistics
1653                "Etfs": [],  # list of dictionaries of all etfs statistics
1654                "Futures": [],  # list of dictionaries of all futures statistics
1655                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1656                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1657                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1658                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1659                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1660            },
1661            "analytics": {  # --- some analytics of portfolio:
1662                "distrByAssets": {},  # portfolio distribution by assets
1663                "distrByCompanies": {},  # portfolio distribution by companies
1664                "distrBySectors": {},  # portfolio distribution by sectors
1665                "distrByCurrencies": {},  # portfolio distribution by currencies
1666                "distrByCountries": {},  # portfolio distribution by countries
1667            }
1668        }
1669
1670        details = details.lower()
1671        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1672        if details not in availableDetails:
1673            details = "full"
1674            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1675
1676        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1677
1678        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1679        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1680        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1681        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1682
1683        # save response headers without "positions" section:
1684        for key in portfolioResponse.keys():
1685            if key != "positions":
1686                view["raw"]["headers"][key] = portfolioResponse[key]
1687
1688            else:
1689                continue
1690
1691        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1692        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1693        for item in portfolioResponse["positions"]:
1694            if item["instrumentType"] == "currency":
1695                self.figi = item["figi"]
1696                curr = self.SearchByFIGI(requestPrice=False)
1697
1698                # current price of currency in RUB:
1699                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1700                    "name": curr["name"],
1701                    "currentPrice": NanoToFloat(
1702                        item["currentPrice"]["units"],
1703                        item["currentPrice"]["nano"]
1704                    ),
1705                }
1706
1707                view["raw"]["Currencies"].append(item)
1708
1709            elif item["instrumentType"] == "share":
1710                view["raw"]["Shares"].append(item)
1711
1712            elif item["instrumentType"] == "bond":
1713                view["raw"]["Bonds"].append(item)
1714
1715            elif item["instrumentType"] == "etf":
1716                view["raw"]["Etfs"].append(item)
1717
1718            elif item["instrumentType"] == "futures":
1719                view["raw"]["Futures"].append(item)
1720
1721            else:
1722                continue
1723
1724        # how many volume of currencies (by ISO currency name) are blocked:
1725        for item in view["raw"]["positions"]["blocked"]:
1726            blocked = NanoToFloat(item["units"], item["nano"])
1727            if blocked > 0:
1728                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1729
1730        # how many volume of instruments (by FIGI) are blocked:
1731        for item in view["raw"]["positions"]["securities"]:
1732            blocked = int(item["blocked"])
1733            if blocked > 0:
1734                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1735
1736        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1737
1738        if "rub" in allBlocked.keys():
1739            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1740
1741        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1742        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1743        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1744        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1745        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1746        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1747        view["stat"]["portfolioCostRUB"] = sum([
1748            view["stat"]["allCurrenciesCostRUB"],
1749            view["stat"]["sharesCostRUB"],
1750            view["stat"]["bondsCostRUB"],
1751            view["stat"]["etfsCostRUB"],
1752            view["stat"]["futuresCostRUB"],
1753        ])
1754
1755        # --- calculating some portfolio statistics:
1756        byComp = {}  # distribution by companies
1757        bySect = {}  # distribution by sectors
1758        byCurr = {}  # distribution by currencies (include RUB)
1759        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1760        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1761
1762        for item in portfolioResponse["positions"]:
1763            self.figi = item["figi"]
1764            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1765
1766            if instrument:
1767                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1768                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1769
1770                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1771                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1772
1773                else:
1774                    blocked = 0
1775
1776                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1777                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1778                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1779                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1780                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1781                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1782                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1783                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1784                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1785                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1786                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1787                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1788
1789                statData = {
1790                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1791                    "ticker": instrument["ticker"],  # ticker by FIGI
1792                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1793                    "volume": volume,  # available volume of instrument
1794                    "lots": lots,  # volume in lots of instrument
1795                    "direction": direction,  # direction of an instrument's position: short or long
1796                    "blocked": blocked,  # blocked volume of currency or instrument
1797                    "currentPrice": curPrice,  # current instrument's price in basic asset
1798                    "average": average,  # current average position price
1799                    "cost": cost,  # current cost of all volume of instrument in basic asset
1800                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1801                    "costRUB": costRUB,  # cost of instrument in ruble
1802                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1803                    "profit": profit,  # expected profit at current moment
1804                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1805                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1806                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1807                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1808                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1809                    "step": instrument["step"],  # minimum price increment
1810                }
1811
1812                # adding distribution by unique countries:
1813                if statData["country"] not in byCountry.keys():
1814                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                else:
1817                    byCountry[statData["country"]]["cost"] += costRUB
1818                    byCountry[statData["country"]]["percent"] += percentCostRUB
1819
1820                if item["instrumentType"] != "currency":
1821                    # adding distribution by unique companies:
1822                    if statData["name"]:
1823                        if statData["name"] not in byComp.keys():
1824                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1825
1826                        else:
1827                            byComp[statData["name"]]["cost"] += costRUB
1828                            byComp[statData["name"]]["percent"] += percentCostRUB
1829
1830                    # adding distribution by unique sectors:
1831                    if statData["sector"] not in bySect.keys():
1832                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1833
1834                    else:
1835                        bySect[statData["sector"]]["cost"] += costRUB
1836                        bySect[statData["sector"]]["percent"] += percentCostRUB
1837
1838                # adding distribution by unique currencies:
1839                if currency not in byCurr.keys():
1840                    byCurr[currency] = {
1841                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1842                        "cost": costRUB,
1843                        "percent": percentCostRUB
1844                    }
1845
1846                else:
1847                    byCurr[currency]["cost"] += costRUB
1848                    byCurr[currency]["percent"] += percentCostRUB
1849
1850                # saving statistics for every instrument:
1851                if item["instrumentType"] == "currency":
1852                    view["stat"]["Currencies"].append(statData)
1853
1854                    # update dict with free funds for trading (total - blocked) by currencies
1855                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1856                    view["stat"]["funds"][currency] = {
1857                        "total": volume,
1858                        "totalCostRUB": costRUB,  # total volume cost in rubles
1859                        "free": volume - blocked,
1860                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1861                    }
1862
1863                elif item["instrumentType"] == "share":
1864                    view["stat"]["Shares"].append(statData)
1865
1866                elif item["instrumentType"] == "bond":
1867                    view["stat"]["Bonds"].append(statData)
1868
1869                elif item["instrumentType"] == "etf":
1870                    view["stat"]["Etfs"].append(statData)
1871
1872                elif item["instrumentType"] == "Futures":
1873                    view["stat"]["Futures"].append(statData)
1874
1875                else:
1876                    continue
1877
1878        # total changes in Russian Ruble:
1879        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1880        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1881        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1882        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1883        view["stat"]["funds"]["rub"] = {
1884            "total": view["stat"]["availableRUB"],
1885            "totalCostRUB": view["stat"]["availableRUB"],
1886            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1888        }
1889
1890        # --- pending orders sector data:
1891        uniquePendingOrders = []
1892        uniquePendingOrdersFIGIs = []
1893        for item in view["raw"]["orders"]:
1894            if item["figi"] not in uniquePendingOrdersFIGIs:
1895                uniquePendingOrdersFIGIs.append(item["figi"])
1896                uniquePendingOrders.append(item)
1897
1898        for item in uniquePendingOrders:
1899            self.figi = item["figi"]
1900            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1901
1902            if instrument:
1903                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1904                orderType = TKS_ORDER_TYPES[item["orderType"]]
1905                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1906                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1907
1908                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1909                if item["direction"] == "ORDER_DIRECTION_BUY":
1910                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1911
1912                else:
1913                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1914
1915                # requested price for order execution:
1916                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1917
1918                # necessary changes in percent to reach target from current price:
1919                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1920
1921                view["stat"]["orders"].append({
1922                    "orderID": item["orderId"],  # orderId number parameter of current order
1923                    "figi": item["figi"],  # FIGI identification
1924                    "ticker": instrument["ticker"],  # ticker name by FIGI
1925                    "lotsRequested": item["lotsRequested"],  # requested lots value
1926                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1927                    "currentPrice": lastPrice,  # current instrument's price for defined action
1928                    "targetPrice": target,  # requested price for order execution in base currency
1929                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1930                    "percentChanges": changes,  # changes in percent to target from current price
1931                    "currency": item["currency"],  # instrument's currency name
1932                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1933                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1934                    "status": orderState,  # order status from TKS_ORDER_STATES
1935                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1936                })
1937
1938        # --- stop orders sector data:
1939        uniqueStopOrders = []
1940        uniqueStopOrdersFIGIs = []
1941        for item in view["raw"]["stopOrders"]:
1942            if item["figi"] not in uniqueStopOrdersFIGIs:
1943                uniqueStopOrdersFIGIs.append(item["figi"])
1944                uniqueStopOrders.append(item)
1945
1946        for item in uniqueStopOrders:
1947            self.figi = item["figi"]
1948            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1949
1950            if instrument:
1951                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1952                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1953                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1954
1955                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1956                if "expirationTime" in item.keys():
1957                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1958                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1959
1960                else:
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1962                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1963
1964                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1965                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1966                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1967
1968                else:
1969                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1970
1971                # requested price when stop-order executed:
1972                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1973
1974                # price for limit-order, set up when stop-order executed:
1975                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1976
1977                # necessary changes in percent to reach target from current price:
1978                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1979
1980                view["stat"]["stopOrders"].append({
1981                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1982                    "figi": item["figi"],  # FIGI identification
1983                    "ticker": instrument["ticker"],  # ticker name by FIGI
1984                    "lotsRequested": item["lotsRequested"],  # requested lots value
1985                    "currentPrice": lastPrice,  # current instrument's price for defined action
1986                    "targetPrice": target,  # requested price for stop-order execution in base currency
1987                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1988                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1989                    "percentChanges": changes,  # changes in percent to target from current price
1990                    "currency": item["currency"],  # instrument's currency name
1991                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1992                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1993                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1994                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1995                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1996                })
1997
1998        # --- calculating data for analytics section:
1999        # portfolio distribution by assets:
2000        view["analytics"]["distrByAssets"] = {
2001            "Ruble": {
2002                "uniques": 1,
2003                "cost": view["stat"]["availableRUB"],
2004                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Currencies": {
2007                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2008                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2009                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011            "Shares": {
2012                "uniques": len(view["stat"]["Shares"]),
2013                "cost": view["stat"]["sharesCostRUB"],
2014                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2015            },
2016            "Bonds": {
2017                "uniques": len(view["stat"]["Bonds"]),
2018                "cost": view["stat"]["bondsCostRUB"],
2019                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2020            },
2021            "Etfs": {
2022                "uniques": len(view["stat"]["Etfs"]),
2023                "cost": view["stat"]["etfsCostRUB"],
2024                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025            },
2026            "Futures": {
2027                "uniques": len(view["stat"]["Futures"]),
2028                "cost": view["stat"]["futuresCostRUB"],
2029                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2030            },
2031        }
2032
2033        # portfolio distribution by companies:
2034        view["analytics"]["distrByCompanies"]["All money cash"] = {
2035            "ticker": "",
2036            "cost": view["stat"]["allCurrenciesCostRUB"],
2037            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038        }
2039        view["analytics"]["distrByCompanies"].update(byComp)
2040
2041        # portfolio distribution by sectors:
2042        view["analytics"]["distrBySectors"]["All money cash"] = {
2043            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2044            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2045        }
2046        view["analytics"]["distrBySectors"].update(bySect)
2047
2048        # portfolio distribution by currencies:
2049        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2050            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2051            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2052
2053        view["analytics"]["distrByCurrencies"].update(byCurr)
2054        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2055        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2056
2057        # portfolio distribution by countries:
2058        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2059            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2060            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2061
2062        view["analytics"]["distrByCountries"].update(byCountry)
2063        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2064        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2065
2066        # --- Prepare text statistics overview in human-readable:
2067        if show:
2068            # Whatever the value `details`, header not changes:
2069            info = [
2070                "# Client's portfolio\n\n",
2071                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2072                "* **Account ID:** [{}]\n".format(self.accountId),
2073            ]
2074
2075            if details in ["full", "positions", "digest"]:
2076                info.extend([
2077                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2078                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2079                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2080                        view["stat"]["totalChangesRUB"],
2081                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2082                        view["stat"]["totalChangesPercentRUB"],
2083                    ),
2084                ])
2085
2086            if details in ["full", "positions"]:
2087                info.extend([
2088                    "## Open positions\n\n",
2089                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2090                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2091                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2092                        "{:.2f} ({:.2f}) rub".format(
2093                            view["stat"]["availableRUB"],
2094                            view["stat"]["blockedRUB"],
2095                        )
2096                    )
2097                ])
2098
2099                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2100                    return [
2101                        "|                             |                                 |          |              |              |                     |                              |\n",
2102                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2103                            noTradeStr if noTradeStr else typeStr,
2104                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2105                        ),
2106                    ]
2107
2108                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2109                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2110                        "{} [{}]".format(data["ticker"], data["figi"]),
2111                        "{:.2f} ({:.2f}) {}".format(
2112                            data["volume"],
2113                            data["blocked"],
2114                            data["currency"],
2115                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2116                            data["volume"],
2117                            data["blocked"],
2118                        ),
2119                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2120                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2121                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2122                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2123                        "{}{:.2f} {} ({}{:.2f}%)".format(
2124                            "+" if data["profit"] > 0 else "",
2125                            data["profit"], data["baseCurrencyName"],
2126                            "+" if data["percentProfit"] > 0 else "",
2127                            data["percentProfit"],
2128                        ),
2129                    )
2130
2131                # --- Show currencies section:
2132                if view["stat"]["Currencies"]:
2133                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2134                    for item in view["stat"]["Currencies"]:
2135                        info.append(_InfoStr(item, showCurrencyName=True))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2139
2140                # --- Show shares section:
2141                if view["stat"]["Shares"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2143
2144                    for item in view["stat"]["Shares"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2149
2150                # --- Show bonds section:
2151                if view["stat"]["Bonds"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2153
2154                    for item in view["stat"]["Bonds"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2159
2160                # --- Show etfs section:
2161                if view["stat"]["Etfs"]:
2162                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2163
2164                    for item in view["stat"]["Etfs"]:
2165                        info.append(_InfoStr(item))
2166
2167                else:
2168                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2169
2170                # --- Show futures section:
2171                if view["stat"]["Futures"]:
2172                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2173
2174                    for item in view["stat"]["Futures"]:
2175                        info.append(_InfoStr(item))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2179
2180            if details in ["full", "orders"]:
2181                # --- Show pending orders section:
2182                if view["stat"]["orders"]:
2183                    info.extend([
2184                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2185                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2186                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2187                    ])
2188
2189                    for item in view["stat"]["orders"]:
2190                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2191                            "{} [{}]".format(item["ticker"], item["figi"]),
2192                            item["orderID"],
2193                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2194                            "{} {} ({}{:.2f}%)".format(
2195                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2196                                item["baseCurrencyName"],
2197                                "+" if item["percentChanges"] > 0 else "",
2198                                float(item["percentChanges"]),
2199                            ),
2200                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2201                            item["action"],
2202                            item["type"],
2203                            item["date"],
2204                        ))
2205
2206                else:
2207                    info.append("\n## Total pending limit-orders: 0\n")
2208
2209                # --- Show stop orders section:
2210                if view["stat"]["stopOrders"]:
2211                    info.extend([
2212                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2213                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2214                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2215                    ])
2216
2217                    for item in view["stat"]["stopOrders"]:
2218                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2219                            "{} [{}]".format(item["ticker"], item["figi"]),
2220                            item["orderID"],
2221                            item["lotsRequested"],
2222                            "{} {} ({}{:.2f}%)".format(
2223                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2224                                item["baseCurrencyName"],
2225                                "+" if item["percentChanges"] > 0 else "",
2226                                float(item["percentChanges"]),
2227                            ),
2228                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2229                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2230                            item["action"],
2231                            item["type"],
2232                            item["expType"],
2233                            item["createDate"],
2234                            item["expDate"],
2235                        ))
2236
2237                else:
2238                    info.append("\n## Total stop-orders: 0\n")
2239
2240            if details in ["full", "analytics"]:
2241                # -- Show analytics section:
2242                if view["stat"]["portfolioCostRUB"] > 0:
2243                    info.extend([
2244                        "\n# Analytics\n"
2245                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2246                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2247                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2248                            view["stat"]["totalChangesRUB"],
2249                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2250                            view["stat"]["totalChangesPercentRUB"],
2251                        ),
2252                        "\n## Portfolio distribution by assets\n"
2253                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2254                        "|------------|---------|---------|--------------------|\n",
2255                    ])
2256
2257                    for key in view["analytics"]["distrByAssets"].keys():
2258                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2259                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2260                                key,
2261                                view["analytics"]["distrByAssets"][key]["uniques"],
2262                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2263                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2264                            ))
2265
2266                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2267                    info.extend([
2268                        "\n## Portfolio distribution by companies\n"
2269                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2270                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2271                    ])
2272
2273                    for company in view["analytics"]["distrByCompanies"].keys():
2274                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2275                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2276                            info.append("| {} | {:<7} | {:<18} |\n".format(
2277                                "{}{}{}".format(
2278                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2279                                    company,
2280                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2281                                ),
2282                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2283                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2284                            ))
2285
2286                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2287                    info.extend([
2288                        "\n## Portfolio distribution by sectors\n"
2289                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2290                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2291                    ])
2292
2293                    for sector in view["analytics"]["distrBySectors"].keys():
2294                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2295                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2296                                sector,
2297                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2298                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2299                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2300                            ))
2301
2302                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2303                    info.extend([
2304                        "\n## Portfolio distribution by currencies\n"
2305                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2306                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2307                    ])
2308
2309                    for curr in view["analytics"]["distrByCurrencies"].keys():
2310                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2311                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2312                            info.append("| {} | {:<7} | {:<18} |\n".format(
2313                                "[{}] {}{}".format(
2314                                    curr,
2315                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2316                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2317                                ),
2318                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2319                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2320                            ))
2321
2322                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2323                    info.extend([
2324                        "\n## Portfolio distribution by countries\n"
2325                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2326                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2327                    ])
2328
2329                    for country in view["analytics"]["distrByCountries"].keys():
2330                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2331                            nameLen = len(country)
2332                            info.append("| {} | {:<7} | {:<18} |\n".format(
2333                                "{}{}".format(
2334                                    country,
2335                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2336                                ),
2337                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2338                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2339                            ))
2340
2341            infoText = "".join(info)
2342
2343            uLogger.info(infoText)
2344
2345            if details == "full" and self.overviewFile:
2346                filename = self.overviewFile
2347
2348            elif details == "digest" and self.overviewDigestFile:
2349                filename = self.overviewDigestFile
2350
2351            elif details == "positions" and self.overviewPositionsFile:
2352                filename = self.overviewPositionsFile
2353
2354            elif details == "orders" and self.overviewOrdersFile:
2355                filename = self.overviewOrdersFile
2356
2357            elif details == "analytics" and self.overviewAnalyticsFile:
2358                filename = self.overviewAnalyticsFile
2359
2360            else:
2361                filename = ""
2362
2363            if filename:
2364                with open(filename, "w", encoding="UTF-8") as fH:
2365                    fH.write(infoText)
2366
2367                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2368
2369        return view
2370
2371    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2372        """
2373        Returns history operations between two given dates for current `accountId`.
2374        If `reportFile` string is not empty then also save human-readable report.
2375        Shows some statistical data of closed positions.
2376
2377        :param start: see docstring in `GetDatesAsString()` method
2378        :param end: see docstring in `GetDatesAsString()` method
2379        :param show: if `True` then also prints all records to the console.
2380        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2381        :return: original list of dictionaries with history of deals records from API ("operations" key):
2382                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2383                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2384        """
2385        if self.accountId is None or not self.accountId:
2386            uLogger.error("Variable `accountId` must be defined for using this method!")
2387            raise Exception("Account ID required")
2388
2389        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2390
2391        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2392
2393        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2394        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2395        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2396        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2397        customStat = {}  # custom statistics in additional to responseJSON
2398
2399        # --- output report in human-readable format:
2400        if show or self.reportFile:
2401            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2402            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2403            nextDay = ""
2404
2405            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2406
2407            if len(ops) > 0:
2408                customStat = {
2409                    "opsCount": 0,  # total operations count
2410                    "buyCount": 0,  # buy operations
2411                    "sellCount": 0,  # sell operations
2412                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2413                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2414                    "payIn": {"rub": 0.},  # Deposit brokerage account
2415                    "payOut": {"rub": 0.},  # Withdrawals
2416                    "divs": {"rub": 0.},  # Dividends income
2417                    "coupons": {"rub": 0.},  # Coupon's income
2418                    "brokerCom": {"rub": 0.},  # Service commissions
2419                    "serviceCom": {"rub": 0.},  # Service commissions
2420                    "marginCom": {"rub": 0.},  # Margin commissions
2421                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2422                }
2423
2424                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2425                for item in ops:
2426                    if item["state"] == "OPERATION_STATE_EXECUTED":
2427                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2428
2429                        # count buy operations:
2430                        if "_BUY" in item["operationType"]:
2431                            customStat["buyCount"] += 1
2432
2433                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2434                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2438
2439                        # count sell operations:
2440                        elif "_SELL" in item["operationType"]:
2441                            customStat["sellCount"] += 1
2442
2443                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2444                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2445
2446                            else:
2447                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2448
2449                        # count incoming operations:
2450                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2451                            if item["payment"]["currency"] in customStat["payIn"].keys():
2452                                customStat["payIn"][item["payment"]["currency"]] += payment
2453
2454                            else:
2455                                customStat["payIn"][item["payment"]["currency"]] = payment
2456
2457                        # count withdrawals operations:
2458                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2459                            if item["payment"]["currency"] in customStat["payOut"].keys():
2460                                customStat["payOut"][item["payment"]["currency"]] += payment
2461
2462                            else:
2463                                customStat["payOut"][item["payment"]["currency"]] = payment
2464
2465                        # count dividends income:
2466                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2467                            if item["payment"]["currency"] in customStat["divs"].keys():
2468                                customStat["divs"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["divs"][item["payment"]["currency"]] = payment
2472
2473                        # count coupon's income:
2474                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2475                            if item["payment"]["currency"] in customStat["coupons"].keys():
2476                                customStat["coupons"][item["payment"]["currency"]] += payment
2477
2478                            else:
2479                                customStat["coupons"][item["payment"]["currency"]] = payment
2480
2481                        # count broker commissions:
2482                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2483                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2484                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2485
2486                            else:
2487                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2488
2489                        # count service commissions:
2490                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2491                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2492                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2493
2494                            else:
2495                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2496
2497                        # count margin commissions:
2498                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2499                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2500                                customStat["marginCom"][item["payment"]["currency"]] += payment
2501
2502                            else:
2503                                customStat["marginCom"][item["payment"]["currency"]] = payment
2504
2505                        # count withholding taxes:
2506                        elif "_TAX" in item["operationType"]:
2507                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2508                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2509
2510                            else:
2511                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2512
2513                        else:
2514                            continue
2515
2516                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2517
2518                # --- view "Actions" lines:
2519                info.extend([
2520                    "| Report sections            |                               |                              |                      |                        |\n",
2521                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2522                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2523                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2524                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2525                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2526                    ),
2527                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2528                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2529                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2530                    ),
2531                ])
2532
2533                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2534                for key in opsKeys:
2535                    if key == "rub":
2536                        continue
2537
2538                    info.extend([
2539                        "|                            |                               | {:<28} |                      |                        |\n".format(
2540                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2541                        ),
2542                        "|                            |                               | {:<28} |                      |                        |\n".format(
2543                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2544                        ),
2545                    ])
2546
2547                info.append(splitLine1)
2548
2549                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2550                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2551                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2552                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2553                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2554                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2555                    )
2556
2557                # --- view "Payments" lines:
2558                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2559                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2560
2561                for key in paymentsKeys:
2562                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2563
2564                info.append(splitLine1)
2565
2566                # --- view "Commissions and taxes" lines:
2567                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2568                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2569
2570                for key in comKeys:
2571                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2572
2573                info.append(splitLine1)
2574
2575                info.extend([
2576                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2577                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2578                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2579                ])
2580
2581            else:
2582                info.append("Broker returned no operations during this period\n")
2583
2584            # --- view "Operations" section:
2585            for item in ops:
2586                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2587                    continue
2588
2589                else:
2590                    self.figi = item["figi"] if item["figi"] else ""
2591                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2592                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2593
2594                    # group of deals during one day:
2595                    if nextDay and item["date"].split("T")[0] != nextDay:
2596                        info.append(splitLine2)
2597                        nextDay = ""
2598
2599                    else:
2600                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2601
2602                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2603                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2604                        self.figi if self.figi else "—",
2605                        instrument["ticker"] if instrument else "—",
2606                        instrument["type"] if instrument else "—",
2607                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2608                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2609                        TKS_OPERATION_STATES[item["state"]],
2610                        TKS_OPERATION_TYPES[item["operationType"]],
2611                    ))
2612
2613            infoText = "".join(info)
2614
2615            if show:
2616                if self.moreDebug:
2617                    uLogger.debug("Records about history of a client's operations successfully received")
2618
2619                uLogger.info(infoText)
2620
2621            if self.reportFile:
2622                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2623                    fH.write(infoText)
2624
2625                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2626
2627        return ops, customStat
2628
2629    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2630        """
2631        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2632
2633        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2634        Warning! Broker server used ISO UTC time by default.
2635
2636        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2637        Also, `historyFile` used to update history with `onlyMissing` parameter.
2638
2639        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2640
2641        :param start: see docstring in `GetDatesAsString()` method.
2642        :param end: see docstring in `GetDatesAsString()` method.
2643        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2644                         `"hour"`, `"day"`. Default: `"hour"`.
2645        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2646                            False by default. Warning! History appends only from last candle to current time
2647                            with always update last candle!
2648        :param csvSep: separator if csv-file is used, `,` by default.
2649        :param show: if `True` then also prints Pandas DataFrame to the console.
2650        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2651                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2652        """
2653        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2654        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2655        history = None  # empty pandas object for history
2656
2657        if interval not in TKS_CANDLE_INTERVALS.keys():
2658            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2659            raise Exception("Incorrect value")
2660
2661        if not (self.ticker or self.figi):
2662            uLogger.error("Ticker or FIGI must be defined!")
2663            raise Exception("Ticker or FIGI required")
2664
2665        if self.ticker and not self.figi:
2666            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2667            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2668
2669        if self.figi and not self.ticker:
2670            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2671            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2672
2673        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2674        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2675        if interval.lower() != "day":
2676            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2677
2678        delta = dtEnd - dtStart  # current UTC time minus last time in file
2679        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2680
2681        # calculate history length in candles:
2682        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2683        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2684            length += 1  # to avoid fraction time
2685
2686        # calculate data blocks count:
2687        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2688
2689        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2690        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2691        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2692        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2693        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2694
2695        tempOld = None  # pandas object for old history, if --only-missing key present
2696        lastTime = None  # datetime object of last old candle in file
2697
2698        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2699            uLogger.debug("--only-missing key present, add only last missing candles...")
2700            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2701
2702            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2703
2704            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2705            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2706            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2707            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2708
2709            # get last datetime object from last string in file or minus 1 delta if file is empty:
2710            if len(tempOld) > 0:
2711                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2712
2713            else:
2714                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2715
2716            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2717
2718        responseJSONs = []  # raw history blocks of data
2719
2720        blockEnd = dtEnd
2721        for item in range(blocks):
2722            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2723            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2724
2725            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2726                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2727            ))
2728
2729            if blockStart == blockEnd:
2730                uLogger.debug("Skipped this zero-length block...")
2731
2732            else:
2733                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2734                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2735                self.body = str({
2736                    "figi": self.figi,
2737                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2738                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2739                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2740                })
2741                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2742
2743                if "code" in responseJSON.keys():
2744                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2745
2746                else:
2747                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2748                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2749
2750                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2751
2752            blockEnd = blockStart
2753
2754        printCount = len(responseJSONs)  # candles to show in console
2755        if responseJSONs:
2756            tempHistory = pd.DataFrame(
2757                data={
2758                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2759                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2760                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2761                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2762                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2763                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2764                    "volume": [int(item["volume"]) for item in responseJSONs],
2765                },
2766                index=range(len(responseJSONs)),
2767                columns=["date", "time", "open", "high", "low", "close", "volume"],
2768            )
2769            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2770            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2771
2772            # append only newest candles to old history if --only-missing key present:
2773            if onlyMissing and tempOld is not None and lastTime is not None:
2774                index = 0  # find start index in tempHistory data:
2775
2776                for i, item in tempHistory.iterrows():
2777                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2778
2779                    if curTime == lastTime:
2780                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2781                        index = i
2782                        printCount = index + 1
2783                        break
2784
2785                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2786
2787            else:
2788                history = tempHistory  # if no `--only-missing` key then load full data from server
2789
2790            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2791
2792        if history is not None and not history.empty:
2793            if show:
2794                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2795                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2796                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2797                ))
2798
2799        else:
2800            uLogger.warning("Received an empty candles history!")
2801
2802        if self.historyFile is not None:
2803            if history is not None and not history.empty:
2804                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2805                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2806
2807            else:
2808                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2809
2810        else:
2811            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2812
2813        return history
2814
2815    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2816        """
2817        Load candles history from csv-file and return Pandas DataFrame object.
2818
2819        See also: `History()` and `ShowHistoryChart()` methods.
2820
2821        :param filePath: path to csv-file to open.
2822        """
2823        loadedHistory = None  # init candles data object
2824
2825        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2826
2827        if os.path.exists(filePath):
2828            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2829
2830            tfStr = self.priceModel.FormattedDelta(
2831                self.priceModel.timeframe,
2832                "{days} days {hours}h {minutes}m {seconds}s",
2833            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2834                self.priceModel.timeframe,
2835                "{hours}h {minutes}m {seconds}s",
2836            )
2837
2838            if loadedHistory is not None and not loadedHistory.empty:
2839                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2840                    len(loadedHistory),
2841                    tfStr,
2842                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2843                )
2844
2845            else:
2846                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2847
2848        else:
2849            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2850
2851        return loadedHistory
2852
2853    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2854        """
2855        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2856
2857        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2858        Default: `index.html` (both for interact and non-interact candlesticks chart).
2859
2860        See also: `History()` and `LoadHistory()` methods.
2861
2862        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2863        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2864                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2865                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2866                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2867        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2868                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2869        """
2870        if isinstance(candles, str):
2871            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2872            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2873
2874        elif isinstance(candles, pd.DataFrame):
2875            self.priceModel.prices = candles  # set candles chain from variable
2876            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2877
2878            if "datetime" not in candles.columns:
2879                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2880
2881        else:
2882            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2883            raise Exception("Incorrect value")
2884
2885        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2886
2887        if interact:
2888            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2889
2890            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2891
2892        else:
2893            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2894
2895            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2896
2897        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2898
2899    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2900        """
2901        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2902        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2903
2904        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2905
2906        :param operation: string "Buy" or "Sell".
2907        :param lots: volume, integer count of lots >= 1.
2908        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2909        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2910        :param expDate: string "Undefined" by default or local date in future,
2911                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2912        :return: JSON with response from broker server.
2913        """
2914        if self.accountId is None or not self.accountId:
2915            uLogger.error("Variable `accountId` must be defined for using this method!")
2916            raise Exception("Account ID required")
2917
2918        if operation is None or not operation or operation not in ("Buy", "Sell"):
2919            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2920            raise Exception("Incorrect value")
2921
2922        if lots is None or lots < 1:
2923            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2924            lots = 1
2925
2926        if tp is None or tp < 0:
2927            tp = 0
2928
2929        if sl is None or sl < 0:
2930            sl = 0
2931
2932        if expDate is None or not expDate:
2933            expDate = "Undefined"
2934
2935        if not (self.ticker or self.figi):
2936            uLogger.error("Ticker or FIGI must be defined!")
2937            raise Exception("Ticker or FIGI required")
2938
2939        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2940        self.ticker = instrument["ticker"]
2941        self.figi = instrument["figi"]
2942
2943        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2944
2945        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2946        self.body = str({
2947            "figi": self.figi,
2948            "quantity": str(lots),
2949            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2950            "accountId": str(self.accountId),
2951            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2952        })
2953        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2954
2955        if "orderId" in response.keys():
2956            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2957                operation, response["orderId"],
2958                self.ticker, self.figi, lots,
2959                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2960                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2961                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2962            ))
2963
2964        else:
2965            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2966
2967        if tp > 0:
2968            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2969
2970        if sl > 0:
2971            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2972
2973        return response
2974
2975    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2976        """
2977        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2978        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2979
2980        See also: `Order()` and `Trade()` docstrings.
2981
2982        :param lots: volume, integer count of lots >= 1.
2983        :param tp: float > 0, take profit price of stop-order.
2984        :param sl: float > 0, stop loss price of stop-order.
2985        :param expDate: it's a local date in future.
2986                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2987        :return: JSON with response from broker server.
2988        """
2989        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2990
2991    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2992        """
2993        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2994        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2995
2996        See also: `Order()` and `Trade()` docstrings.
2997
2998        :param lots: volume, integer count of lots >= 1.
2999        :param tp: float > 0, take profit price of stop-order.
3000        :param sl: float > 0, stop loss price of stop-order.
3001        :param expDate: it's a local date in the future.
3002                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3003        :return: JSON with response from broker server.
3004        """
3005        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3006
3007    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3008        """
3009        Close position of given instruments.
3010
3011        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3012        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3013                         This avoids unnecessary downloading data from the server.
3014        """
3015        if instruments is None or not instruments:
3016            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3017            raise Exception("Ticker or FIGI required")
3018
3019        if isinstance(instruments, str):
3020            instruments = [instruments]
3021
3022        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3023        if uniqueInstruments:
3024            if portfolio is None or not portfolio:
3025                portfolio = self.Overview(show=False)
3026
3027            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3028            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3029
3030            for self.figi in uniqueInstruments:
3031                if self.figi not in allOpened:
3032                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3033                    continue
3034
3035                # search open trade info about instrument by ticker:
3036                instrument = {}
3037                for iType in TKS_INSTRUMENTS:
3038                    if instrument:
3039                        break
3040
3041                    for item in portfolio["stat"][iType]:
3042                        if item["figi"] == self.figi:
3043                            instrument = item
3044                            break
3045
3046                if instrument:
3047                    self.ticker = instrument["ticker"]
3048                    self.figi = instrument["figi"]
3049
3050                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3051                        self.ticker,
3052                        self.figi,
3053                        int(instrument["volume"]),
3054                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3055                    ))
3056
3057                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3058
3059                    if tradeLots > 0:
3060                        if instrument["blocked"] > 0:
3061                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3062                                instrument["blocked"],
3063                                self.ticker,
3064                                tradeLots,
3065                            ))
3066
3067                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3068                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3069
3070                    else:
3071                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3072
3073    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3074        """
3075        Close all positions of given instruments with defined type.
3076
3077        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3078        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3079                         This avoids unnecessary downloading data from the server.
3080        """
3081        if iType not in TKS_INSTRUMENTS:
3082            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3083
3084        else:
3085            if portfolio is None or not portfolio:
3086                portfolio = self.Overview(show=False)
3087
3088            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3089            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3090
3091            if tickers and portfolio:
3092                self.CloseTrades(tickers, portfolio)
3093
3094            else:
3095                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3096
3097    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3098        """
3099        Universal method to create market or limit orders with all available parameters for current `accountId`.
3100        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3101
3102        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3103        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3104
3105        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3106        then broker immediately open market order as you can do simple --buy or --sell operations!
3107
3108        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3109        When current price will go up or down to target price value then broker opens a limit order.
3110        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3111
3112        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3113
3114        :param operation: string "Buy" or "Sell".
3115        :param orderType: string "Limit" or "Stop".
3116        :param lots: volume, integer count of lots >= 1.
3117        :param targetPrice: target price > 0. This is open trade price for limit order.
3118        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3119                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3120        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3121                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3122                         Stop loss order always executed by market price.
3123        :param expDate: string "Undefined" by default or local date in future.
3124                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3125                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3126                        A limit order has no expiration date, it lasts until the end of the trading day.
3127        :return: JSON with response from broker server.
3128        """
3129        if self.accountId is None or not self.accountId:
3130            uLogger.error("Variable `accountId` must be defined for using this method!")
3131            raise Exception("Account ID required")
3132
3133        if operation is None or not operation or operation not in ("Buy", "Sell"):
3134            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3135            raise Exception("Incorrect value")
3136
3137        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3138            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3139            raise Exception("Incorrect value")
3140
3141        if lots is None or lots < 1:
3142            uLogger.error("You must define trade volume > 0: integer count of lots!")
3143            raise Exception("Incorrect value")
3144
3145        if targetPrice is None or targetPrice <= 0:
3146            uLogger.error("Target price for limit-order must be greater than 0!")
3147            raise Exception("Incorrect value")
3148
3149        if limitPrice is None or limitPrice <= 0:
3150            limitPrice = targetPrice
3151
3152        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3153            stopType = "Limit"
3154
3155        if expDate is None or not expDate:
3156            expDate = "Undefined"
3157
3158        if not (self.ticker or self.figi):
3159            uLogger.error("Tocker or FIGI must be defined!")
3160            raise Exception("Ticker or FIGI required")
3161
3162        response = {}
3163        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3164        self.ticker = instrument["ticker"]
3165        self.figi = instrument["figi"]
3166
3167        if orderType == "Limit":
3168            uLogger.debug(
3169                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3170                    self.ticker, self.figi,
3171                    operation, lots, targetPrice, instrument["currency"],
3172                ))
3173
3174            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3175            self.body = str({
3176                "figi": self.figi,
3177                "quantity": str(lots),
3178                "price": FloatToNano(targetPrice),
3179                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3180                "accountId": str(self.accountId),
3181                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3182            })
3183            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3184
3185            if "orderId" in response.keys():
3186                uLogger.info(
3187                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3188                        response["orderId"],
3189                        self.ticker, self.figi,
3190                        operation, lots, targetPrice, instrument["currency"],
3191                    ))
3192
3193                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3194                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3195                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3196                            targetPrice, instrument["currency"],
3197                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3198                        ))
3199
3200                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3201                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3202                            targetPrice, instrument["currency"],
3203                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3204                        ))
3205
3206            else:
3207                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3208
3209        if orderType == "Stop":
3210            uLogger.debug(
3211                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3212                    self.ticker, self.figi,
3213                    operation, lots,
3214                    targetPrice, instrument["currency"],
3215                    limitPrice, instrument["currency"],
3216                    stopType, expDate,
3217                ))
3218
3219            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3220            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3221            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3222
3223            body = {
3224                "figi": self.figi,
3225                "quantity": str(lots),
3226                "price": FloatToNano(limitPrice),
3227                "stopPrice": FloatToNano(targetPrice),
3228                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3229                "accountId": str(self.accountId),
3230                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3231                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3232            }
3233
3234            if expDateUTC:
3235                body["expireDate"] = expDateUTC
3236
3237            self.body = str(body)
3238            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3239
3240            if "stopOrderId" in response.keys():
3241                uLogger.info(
3242                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3243                        response["stopOrderId"],
3244                        self.ticker, self.figi,
3245                        operation, lots,
3246                        targetPrice, instrument["currency"],
3247                        limitPrice, instrument["currency"],
3248                        TKS_STOP_ORDER_TYPES[stopOrderType],
3249                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3250                    ))
3251
3252                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3253                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3254                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3255                            targetPrice, instrument["currency"],
3256                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3257                        ))
3258
3259                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3260                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3261                            targetPrice, instrument["currency"],
3262                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3263                        ))
3264
3265            else:
3266                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3267
3268        return response
3269
3270    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3271        """
3272        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3273        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3274        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3275        See also: `Order()` docstring.
3276
3277        :param lots: volume, integer count of lots >= 1.
3278        :param targetPrice: target price > 0. This is open trade price for limit order.
3279        :return: JSON with response from broker server.
3280        """
3281        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3282
3283    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3284        """
3285        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3286        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3287        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3288        target price value then broker opens a limit order. See also: `Order()` docstring.
3289
3290        :param lots: volume, integer count of lots >= 1.
3291        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3292        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3293                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3294        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3295                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3296        :param expDate: string "Undefined" by default or local date in future.
3297                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3298                        This date is converting to UTC format for server.
3299        :return: JSON with response from broker server.
3300        """
3301        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3302
3303    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3304        """
3305        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3306        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3307        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3308        See also: `Order()` docstring.
3309
3310        :param lots: volume, integer count of lots >= 1.
3311        :param targetPrice: target price > 0. This is open trade price for limit order.
3312        :return: JSON with response from broker server.
3313        """
3314        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3315
3316    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3317        """
3318        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3319        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3320        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3321        target price value then broker opens a limit order. See also: `Order()` docstring.
3322
3323        :param lots: volume, integer count of lots >= 1.
3324        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3325        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3326                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3327        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3328                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3329        :param expDate: string "Undefined" by default or local date in future.
3330                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3331                        This date is converting to UTC format for server.
3332        :return: JSON with response from broker server.
3333        """
3334        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3335
3336    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3337        """
3338        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3339
3340        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3341        :param allOrdersIDs: pre-received lists of all active pending orders.
3342                             This avoids unnecessary downloading data from the server.
3343        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3344        """
3345        if self.accountId is None or not self.accountId:
3346            uLogger.error("Variable `accountId` must be defined for using this method!")
3347            raise Exception("Account ID required")
3348
3349        if orderIDs:
3350            if allOrdersIDs is None or not allOrdersIDs:
3351                rawOrders = self.RequestPendingOrders()
3352                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3353
3354            if allStopOrdersIDs is None or not allStopOrdersIDs:
3355                rawStopOrders = self.RequestStopOrders()
3356                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3357
3358            for orderID in orderIDs:
3359                idInPendingOrders = orderID in allOrdersIDs
3360                idInStopOrders = orderID in allStopOrdersIDs
3361
3362                if not (idInPendingOrders or idInStopOrders):
3363                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3364                    continue
3365
3366                else:
3367                    if idInPendingOrders:
3368                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3369
3370                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3371                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3372                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3373                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3374
3375                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3376                            if self.moreDebug:
3377                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3378
3379                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3380
3381                        else:
3382                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3383
3384                    elif idInStopOrders:
3385                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3386
3387                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3388                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3389                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3390                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3391
3392                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3393                            if self.moreDebug:
3394                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3395
3396                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3397
3398                        else:
3399                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3400
3401                    else:
3402                        continue
3403
3404    def CloseAllOrders(self) -> None:
3405        """
3406        Gets a list of open pending and stop orders and cancel it all.
3407        """
3408        rawOrders = self.RequestPendingOrders()
3409        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3410        lenOrders = len(allOrdersIDs)
3411
3412        rawStopOrders = self.RequestStopOrders()
3413        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3414        lenSOrders = len(allStopOrdersIDs)
3415
3416        if lenOrders > 0 or lenSOrders > 0:
3417            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3418
3419            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3420
3421        else:
3422            uLogger.info("Orders not found, nothing to cancel.")
3423
3424    def CloseAll(self, *args) -> None:
3425        """
3426        Close all available (not blocked) opened trades and orders.
3427
3428        Also, you can select one or more keywords case-insensitive:
3429        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3430
3431        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3432        """
3433        overview = self.Overview(show=False)  # get all open trades info
3434
3435        if len(args) == 0:
3436            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3437            self.CloseAllOrders()  # close all pending and stop orders
3438
3439            for iType in TKS_INSTRUMENTS:
3440                if iType != "Currencies":
3441                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3442
3443        else:
3444            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3445            lowerArgs = [x.lower() for x in args]
3446
3447            if "orders" in lowerArgs:
3448                self.CloseAllOrders()  # close all pending and stop orders
3449
3450            for iType in TKS_INSTRUMENTS:
3451                if iType.lower() in lowerArgs and iType != "Currencies":
3452                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3453
3454    @staticmethod
3455    def ParseOrderParameters(operation, **inputParameters):
3456        """
3457        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3458
3459        :param operation: string "Buy" or "Sell".
3460        :param inputParameters: this is dict of strings that looks like this
3461               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3462               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3463               "prices" key: one or more prices to open limit-orders
3464               Counts of values in lots and prices lists must be equals!
3465        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3466        """
3467        # TODO: update order grid work with api v2
3468        pass
3469        # uLogger.debug("Input parameters: {}".format(inputParameters))
3470        #
3471        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3472        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3473        #     raise Exception("Incorrect value")
3474        #
3475        # if "l" in inputParameters.keys():
3476        #     inputParameters["lots"] = inputParameters.pop("l")
3477        #
3478        # if "p" in inputParameters.keys():
3479        #     inputParameters["prices"] = inputParameters.pop("p")
3480        #
3481        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3482        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3483        #     raise Exception("Incorrect value")
3484        #
3485        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3486        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3487        #
3488        # if len(lots) != len(prices):
3489        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3490        #     raise Exception("Incorrect value")
3491        #
3492        # uLogger.debug("Extracted parameters for orders:")
3493        # uLogger.debug("lots = {}".format(lots))
3494        # uLogger.debug("prices = {}".format(prices))
3495        #
3496        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3497        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3498        # uLogger.debug("Order parameters: {}".format(result))
3499        #
3500        # return result
3501
3502    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3503        """
3504        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3505
3506        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3507        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3508        """
3509        result = False
3510        msg = "Instrument not defined!"
3511
3512        if portfolio is None or not portfolio:
3513            portfolio = self.Overview(show=False)
3514
3515        if self.ticker:
3516            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3517            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3518
3519            for iType in TKS_INSTRUMENTS:
3520                for instrument in portfolio["stat"][iType]:
3521                    if instrument["ticker"] == self.ticker:
3522                        result = True
3523                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3524                        break
3525
3526        elif self.figi:
3527            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3528            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["figi"] == self.figi:
3533                        result = True
3534                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3535                        break
3536
3537        else:
3538            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3539
3540        uLogger.debug(msg)
3541
3542        return result
3543
3544    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3545        """
3546        Returns instrument is in the user's portfolio if it presents there.
3547        Instrument must be defined by `ticker` (highly priority) or `figi`.
3548
3549        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3550        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3551        """
3552        result = None
3553        msg = "Instrument not defined!"
3554
3555        if portfolio is None or not portfolio:
3556            portfolio = self.Overview(show=False)
3557
3558        if self.ticker:
3559            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3560            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3561
3562            for iType in TKS_INSTRUMENTS:
3563                for instrument in portfolio["stat"][iType]:
3564                    if instrument["ticker"] == self.ticker:
3565                        result = instrument
3566                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3567                        break
3568
3569        elif self.figi:
3570            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3571            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3572
3573            for iType in TKS_INSTRUMENTS:
3574                for instrument in portfolio["stat"][iType]:
3575                    if instrument["figi"] == self.figi:
3576                        result = instrument
3577                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3578                        break
3579
3580        else:
3581            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3582
3583        uLogger.debug(msg)
3584
3585        return result
3586
3587    def RequestLimits(self) -> dict:
3588        """
3589        Method for obtaining the available funds for withdrawal for current `accountId`.
3590
3591        See also:
3592        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3593        - `OverviewLimits()` method
3594
3595        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3596                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3597                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3598                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3599        """
3600        if self.accountId is None or not self.accountId:
3601            uLogger.error("Variable `accountId` must be defined for using this method!")
3602            raise Exception("Account ID required")
3603
3604        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3605
3606        self.body = str({"accountId": self.accountId})
3607        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3608        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3609
3610        if self.moreDebug:
3611            uLogger.debug("Records about available funds for withdrawal successfully received")
3612
3613        return rawLimits
3614
3615    def OverviewLimits(self, show: bool = False) -> dict:
3616        """
3617        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3618
3619        See also: `RequestLimits()`.
3620
3621        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3622        :return: dict with raw parsed data from server and some calculated statistics about it.
3623        """
3624        if self.accountId is None or not self.accountId:
3625            uLogger.error("Variable `accountId` must be defined for using this method!")
3626            raise Exception("Account ID required")
3627
3628        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3629
3630        view = {
3631            "rawLimits": rawLimits,
3632            "limits": {  # parsed data for every currency:
3633                "money": {  # this is an array of portfolio currency positions
3634                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3635                },
3636                "blocked": {  # this is an array of blocked currency
3637                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3638                },
3639                "blockedGuarantee": {  # this is locked money under collateral for futures
3640                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3641                },
3642            },
3643        }
3644
3645        # --- Prepare text table with limits in human-readable format:
3646        if show:
3647            info = [
3648                "# Withdrawal limits\n\n",
3649                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3650                "* **Account ID:** [{}]\n".format(self.accountId),
3651            ]
3652
3653            if view["limits"]["money"]:
3654                info.extend([
3655                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3656                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3657                ])
3658
3659            else:
3660                info.append("\nNo withdrawal limits\n")
3661
3662            for curr in view["limits"]["money"].keys():
3663                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3664                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3665                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3666
3667                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3668                    "[{}]".format(curr),
3669                    "{:.2f}".format(view["limits"]["money"][curr]),
3670                    "{:.2f}".format(availableMoney),
3671                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3672                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3673                )
3674
3675                if curr == "rub":
3676                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3677
3678                else:
3679                    info.append(infoStr)
3680
3681            infoText = "".join(info)
3682
3683            uLogger.info(infoText)
3684
3685            if self.withdrawalLimitsFile:
3686                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3687                    fH.write(infoText)
3688
3689                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3690
3691        return view
3692
3693    def RequestAccounts(self) -> dict:
3694        """
3695        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3696
3697        See also:
3698        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3699        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3700        - `OverviewUserInfo()` method
3701
3702        :return: dict with raw data from server that contains accounts info. Example of dict:
3703                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3704                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3705                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3706                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3707        """
3708        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3709
3710        self.body = str({})
3711        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3712        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3713
3714        if self.moreDebug:
3715            uLogger.debug("Records about available accounts successfully received")
3716
3717        return rawAccounts
3718
3719    def RequestUserInfo(self) -> dict:
3720        """
3721        Method for requesting common user's information.
3722
3723        See also:
3724        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3725        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3726        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3727        - `OverviewUserInfo()` method
3728
3729        :return: dict with raw data from server that contains user's information. Example of dict:
3730                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3731                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3732        """
3733        uLogger.debug("Requesting common user's information. Wait, please...")
3734
3735        self.body = str({})
3736        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3737        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3738
3739        if self.moreDebug:
3740            uLogger.debug("Records about current user successfully received")
3741
3742        return rawUserInfo
3743
3744    def RequestMarginStatus(self, accountId: str = None) -> dict:
3745        """
3746        Method for requesting margin calculation for defined account ID.
3747
3748        See also:
3749        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3750        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3751        - `OverviewUserInfo()` method
3752
3753        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3754        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3755                 Example of responses:
3756                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3757                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3758                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3759                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3760                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3761                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3762        """
3763        if accountId is None or not accountId:
3764            if self.accountId is None or not self.accountId:
3765                uLogger.error("Variable `accountId` must be defined for using this method!")
3766                raise Exception("Account ID required")
3767
3768            else:
3769                accountId = self.accountId  # use `self.accountId` (main ID) by default
3770
3771        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3772
3773        self.body = str({"accountId": accountId})
3774        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3775        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3776
3777        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3778            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3779            rawMargin = {}
3780
3781        else:
3782            if self.moreDebug:
3783                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3784
3785        return rawMargin
3786
3787    def RequestTariffLimits(self) -> dict:
3788        """
3789        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3790
3791        See also:
3792        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3793        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3794        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3795        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3796        - `OverviewUserInfo()` method
3797
3798        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3799                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3800                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3801        """
3802        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3803
3804        self.body = str({})
3805        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3806        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3807
3808        if self.moreDebug:
3809            uLogger.debug("Records with limits of current tariff successfully received")
3810
3811        return rawTariffLimits
3812
3813    def RequestBondCoupons(self, iJSON: dict) -> dict:
3814        """
3815        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3816        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3817        All dates are in UTC timezone.
3818
3819        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3820        Documentation:
3821        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3822        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3823
3824        See also: `ExtendBondsData()`.
3825
3826        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3827                      If raw iJSON is not data of bond then server returns an error [400] with message:
3828                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3829        :return: dictionary with bond payment calendar. Response example
3830                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3831                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3832                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3833                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3834        """
3835        if iJSON["figi"] is None or not iJSON["figi"]:
3836            uLogger.error("FIGI must be defined for using this method!")
3837            raise Exception("FIGI required")
3838
3839        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3840        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3841
3842        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3843            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3844            self.figi,
3845            startDate,
3846            endDate,
3847        ))
3848
3849        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3850        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3851        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3852
3853        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3854            uLogger.warning("Instrument type is not bond!")
3855
3856        else:
3857            if self.moreDebug:
3858                uLogger.debug("Records about bond payment calendar successfully received")
3859
3860        return calendar
3861
3862    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3863        """
3864        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3865        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3866        coupon yields, current yields and some statistics etc.
3867
3868        WARNING! This is too long operation if a lot of bonds requested from broker server.
3869
3870        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3871
3872        :param instruments: list of strings with tickers or FIGIs.
3873        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3874                     for further used by data scientists or stock analytics.
3875        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3876                 In XLSX-file and Pandas DataFrame fields mean:
3877                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3878                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3879        """
3880        if instruments is None or not instruments:
3881            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3882            raise Exception("Ticker or FIGI required")
3883
3884        if isinstance(instruments, str):
3885            instruments = [instruments]
3886
3887        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3888
3889        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3890
3891        iCount = len(uniqueInstruments)
3892        tooLong = iCount >= 20
3893        if tooLong:
3894            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3895
3896        bonds = None
3897        for i, self.figi in enumerate(uniqueInstruments):
3898            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3899
3900            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3901                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3902                rawBond = self.SearchByFIGI(requestPrice=True)
3903
3904                # Widen raw data with UTC current time (iData["actualDateTime"]):
3905                actualDate = datetime.now(tzutc())
3906                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3907
3908                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3909                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3910
3911                # Replace some values with human-readable:
3912                iData["nominalCurrency"] = iData["nominal"]["currency"]
3913                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3914                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3915                iData["aciCurrency"] = iData["aciValue"]["currency"]
3916                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3917                iData["issueSize"] = int(iData["issueSize"])
3918                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3919                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3920                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3921                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3922                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3923                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3924                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3925                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3926                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3927                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3928
3929                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3930                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3931                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3932                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3933                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3934                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3935                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3936                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3937                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3938                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3939                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3940
3941                # Widen raw data with calendar data from `rawCalendar` values:
3942                calendarData = []
3943                if "events" in iData["rawCalendar"].keys():
3944                    for item in iData["rawCalendar"]["events"]:
3945                        calendarData.append({
3946                            "couponDate": item["couponDate"],
3947                            "couponNumber": int(item["couponNumber"]),
3948                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3949                            "payCurrency": item["payOneBond"]["currency"],
3950                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3951                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3952                            "couponStartDate": item["couponStartDate"],
3953                            "couponEndDate": item["couponEndDate"],
3954                            "couponPeriod": item["couponPeriod"],
3955                        })
3956
3957                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3958                    if "maturityDate" not in iData.keys():
3959                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3960
3961                # Widen raw data with Coupon Rate.
3962                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3963                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3964                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3965                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3966
3967                # Widen raw data with Yield to Maturity (YTM) on current date.
3968                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3969                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3970                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3971                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3972                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3973                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3974
3975                iData["calendar"] = calendarData  # adds calendar at the end
3976
3977                # Remove not used data:
3978                iData.pop("uid")
3979                iData.pop("positionUid")
3980                iData.pop("currentPrice")
3981                iData.pop("rawCalendar")
3982
3983                colNames = list(iData.keys())
3984                if bonds is None:
3985                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3986
3987                else:
3988                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3989
3990            else:
3991                uLogger.warning("Instrument is not a bond!")
3992
3993            processed = round(100 * (i + 1) / iCount, 1)
3994            if tooLong and processed % 5 == 0:
3995                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3996
3997            else:
3998                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3999
4000        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4001
4002        # Saving bonds from Pandas DataFrame to XLSX sheet:
4003        if xlsx and self.bondsXLSXFile:
4004            with pd.ExcelWriter(
4005                    path=self.bondsXLSXFile,
4006                    date_format=TKS_DATE_FORMAT,
4007                    datetime_format=TKS_DATE_TIME_FORMAT,
4008                    mode="w",
4009            ) as writer:
4010                bonds.to_excel(
4011                    writer,
4012                    sheet_name="Extended bonds data",
4013                    index=True,
4014                    encoding="UTF-8",
4015                    freeze_panes=(1, 1),
4016                )  # saving as XLSX-file with freeze first row and column as headers
4017
4018            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4019
4020        return bonds
4021
4022    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4023        """
4024        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4025
4026        WARNING! This is too long operation if a lot of bonds requested from broker server.
4027
4028        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4029
4030        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4031                        extended information about bonds: main info, current prices, bond payment calendar,
4032                        coupon yields, current yields and some statistics etc.
4033                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4034        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4035                     for further used by data scientists or stock analytics.
4036        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4037        """
4038        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4039            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4040
4041        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4042
4043        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4044        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4045        calendar = None
4046        for bond in extBonds.iterrows():
4047            for item in bond[1]["calendar"]:
4048                cData = {
4049                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4050                    "couponDate": item["couponDate"],
4051                    "figi": bond[1]["figi"],
4052                    "ticker": bond[1]["ticker"],
4053                    "name": bond[1]["name"],
4054                    "couponNumber": item["couponNumber"],
4055                    "payOneBond": item["payOneBond"],
4056                    "payCurrency": item["payCurrency"],
4057                    "couponType": item["couponType"],
4058                    "couponPeriod": item["couponPeriod"],
4059                    "fixDate": item["fixDate"],
4060                    "couponStartDate": item["couponStartDate"],
4061                    "couponEndDate": item["couponEndDate"],
4062                }
4063
4064                if calendar is None:
4065                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4066
4067                else:
4068                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4069
4070        if calendar is not None:
4071            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4072
4073            # Saving calendar from Pandas DataFrame to XLSX sheet:
4074            if xlsx:
4075                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4076
4077                with pd.ExcelWriter(
4078                        path=xlsxCalendarFile,
4079                        date_format=TKS_DATE_FORMAT,
4080                        datetime_format=TKS_DATE_TIME_FORMAT,
4081                        mode="w",
4082                ) as writer:
4083                    humanReadable = calendar.copy(deep=True)
4084                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4085                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4086                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4087                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4088                    humanReadable.columns = colNames  # human-readable column names
4089
4090                    humanReadable.to_excel(
4091                        writer,
4092                        sheet_name="Bond payments calendar",
4093                        index=False,
4094                        encoding="UTF-8",
4095                        freeze_panes=(1, 2),
4096                    )  # saving as XLSX-file with freeze first row and column as headers
4097
4098                    del humanReadable  # release df in memory
4099
4100                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4101
4102        return calendar
4103
4104    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4105        """
4106        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4107        Also, creates Markdown file with calendar data, `calendar.md` by default.
4108
4109        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4110
4111        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4112                        extended information about bonds: main info, current prices, bond payment calendar,
4113                        coupon yields, current yields and some statistics etc.
4114                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4115        :param show: if `True` then also printing bonds payment calendar to the console,
4116                     otherwise save to file `calendarFile` only. `False` by default.
4117        :return: multilines text in Markdown format with bonds payment calendar as a table.
4118        """
4119        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4120            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4121
4122        infoText = "# Bond payments calendar\n\n"
4123
4124        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4125
4126        if not (calendar is None or calendar.empty):
4127            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4128
4129            info = [
4130                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4131                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4132            ]
4133
4134            newMonth = False
4135            notOneBond = calendar["figi"].nunique() > 1
4136            for i, bond in enumerate(calendar.iterrows()):
4137                if newMonth and notOneBond:
4138                    info.append(splitLine)
4139
4140                info.append(
4141                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4142                        "  √" if bond[1]["paid"] else "  —",
4143                        bond[1]["couponDate"].split("T")[0],
4144                        bond[1]["figi"],
4145                        bond[1]["ticker"],
4146                        bond[1]["couponNumber"],
4147                        "{} {}".format(
4148                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4149                            bond[1]["payCurrency"],
4150                        ),
4151                        bond[1]["couponType"],
4152                        bond[1]["couponPeriod"],
4153                        bond[1]["fixDate"].split("T")[0],
4154                    )
4155                )
4156
4157                if i < len(calendar.values) - 1:
4158                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4159                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4160                    newMonth = False if curDate.month == nextDate.month else True
4161
4162                else:
4163                    newMonth = False
4164
4165            infoText += "".join(info)
4166
4167            if show:
4168                uLogger.info("{}".format(infoText))
4169
4170            if self.calendarFile is not None:
4171                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4172                    fH.write(infoText)
4173
4174                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4175
4176        else:
4177            infoText += "No data\n"
4178
4179        return infoText
4180
4181    def OverviewAccounts(self, show: bool = False) -> dict:
4182        """
4183        Method for parsing and show simple table with all available user accounts.
4184
4185        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4186
4187        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4188        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4189                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4190                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4191                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4192                                                        "closed": "—", "access": "Full access" }, ...}}`
4193        """
4194        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4195
4196        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4197        accounts = {
4198            item["id"]: {
4199                "type": TKS_ACCOUNT_TYPES[item["type"]],
4200                "name": item["name"],
4201                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4202                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4203                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4204                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4205            } for item in rawAccounts["accounts"]
4206        }
4207
4208        # Raw and parsed data with some fields replaced in "stat" section:
4209        view = {
4210            "rawAccounts": rawAccounts,
4211            "stat": accounts,
4212        }
4213
4214        # --- Prepare simple text table with only accounts data in human-readable format:
4215        if show:
4216            info = [
4217                "# User accounts\n\n",
4218                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4219                "| Account ID   | Type                      | Status                    | Name                           |\n",
4220                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4221            ]
4222
4223            for account in view["stat"].keys():
4224                info.extend([
4225                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4226                        account,
4227                        view["stat"][account]["type"],
4228                        view["stat"][account]["status"],
4229                        view["stat"][account]["name"],
4230                    )
4231                ])
4232
4233            infoText = "".join(info)
4234
4235            uLogger.info(infoText)
4236
4237            if self.userAccountsFile:
4238                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4239                    fH.write(infoText)
4240
4241                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4242
4243        return view
4244
4245    def OverviewUserInfo(self, show: bool = False) -> dict:
4246        """
4247        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4248
4249        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4250
4251        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4252        :return: dict with raw parsed data from server and some calculated statistics about it.
4253        """
4254        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4255        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4256        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4257        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4258        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4259        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4260
4261        # This is dict with parsed common user data:
4262        userInfo = {
4263            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4264            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4265            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4266            "tariff": rawUserInfo["tariff"],
4267        }
4268
4269        # This is an array of dict with parsed margin statuses for every account IDs:
4270        margins = {}
4271        for accountId in accounts.keys():
4272            if rawMargins[accountId]:
4273                margins[accountId] = {
4274                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4275                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4276                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4277                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4278                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4279                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4280                }
4281
4282            else:
4283                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4284
4285        unary = {}  # unary-connection limits
4286        for item in rawTariffLimits["unaryLimits"]:
4287            if item["limitPerMinute"] in unary.keys():
4288                unary[item["limitPerMinute"]].extend(item["methods"])
4289
4290            else:
4291                unary[item["limitPerMinute"]] = item["methods"]
4292
4293        stream = {}  # stream-connection limits
4294        for item in rawTariffLimits["streamLimits"]:
4295            if item["limit"] in stream.keys():
4296                stream[item["limit"]].extend(item["streams"])
4297
4298            else:
4299                stream[item["limit"]] = item["streams"]
4300
4301        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4302        limits = {
4303            "unary": unary,
4304            "stream": stream,
4305        }
4306
4307        # Raw and parsed data as an output result:
4308        view = {
4309            "rawUserInfo": rawUserInfo,
4310            "rawAccounts": rawAccounts,
4311            "rawMargins": rawMargins,
4312            "rawTariffLimits": rawTariffLimits,
4313            "stat": {
4314                "userInfo": userInfo,
4315                "accounts": accounts,
4316                "margins": margins,
4317                "limits": limits,
4318            },
4319        }
4320
4321        # --- Prepare text table with user information in human-readable format:
4322        if show:
4323            info = [
4324                "# Full user information\n\n",
4325                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4326                "## Common information\n\n",
4327                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4328                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4329                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4330                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4331                "\n## User accounts\n\n",
4332            ]
4333
4334            for account in view["stat"]["accounts"].keys():
4335                info.extend([
4336                    "### ID: [{}]\n\n".format(account),
4337                    "| Parameters           | Values                                                       |\n",
4338                    "|----------------------|--------------------------------------------------------------|\n",
4339                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4340                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4341                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4342                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4343                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4344                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4345                ])
4346
4347                if margins[account]:
4348                    info.extend([
4349                        "| Margin status:       | Enabled                                                      |\n",
4350                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4351                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4352                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4353                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4354                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4355                    ])
4356
4357                else:
4358                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4359
4360            info.extend([
4361                "\n## Current user tariff limits\n",
4362                "\nSee also:\n",
4363                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4364                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4365                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4366                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4367                "\n### Unary limits\n",
4368            ])
4369
4370            if unary:
4371                for key, values in sorted(unary.items()):
4372                    info.append("\n* Max requests per minute: {}\n".format(key))
4373
4374                    for value in values:
4375                        info.append("  - {}\n".format(value))
4376
4377            else:
4378                info.append("\nNot available\n")
4379
4380            info.append("\n### Stream limits\n")
4381
4382            if stream:
4383                for key, values in sorted(stream.items()):
4384                    info.append("\n* Max stream connections: {}\n".format(key))
4385
4386                    for value in values:
4387                        info.append("  - {}\n".format(value))
4388
4389            else:
4390                info.append("\nNot available\n")
4391
4392            infoText = "".join(info)
4393
4394            uLogger.info(infoText)
4395
4396            if self.userInfoFile:
4397                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4398                    fH.write(infoText)
4399
4400                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4401
4402        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
253
254        See also: `SearchByTicker()`, `SearchInstruments()`.
255        """
256
257        self.figi = ""
258        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
259
260        See also: `SearchByFIGI()`, `SearchInstruments()`.
261        """
262
263        self.depth = 1
264        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
265
266        See also: `GetCurrentPrices()`.
267        """
268
269        self.server = r"https://invest-public-api.tinkoff.ru/rest"
270        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
271
272        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
273        """
274
275        uLogger.debug("Broker API server: {}".format(self.server))
276
277        self.timeout = 15
278        """Server operations timeout in seconds. Default: `15`.
279
280        See also: `SendAPIRequest()`.
281        """
282
283        self.headers = {
284            "Content-Type": "application/json",
285            "accept": "application/json",
286            "Authorization": "Bearer {}".format(self.token),
287            "x-app-name": "Tim55667757.TKSBrokerAPI",
288        }
289        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
290
291        See also: `SendAPIRequest()`.
292        """
293
294        self.body = None
295        """Request body which send to broker server. Default: `None`.
296
297        See also: `SendAPIRequest()`.
298        """
299
300        self.moreDebug = False
301        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
302
303        self.historyFile = None
304        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
305
306        See also: `History()`.
307        """
308
309        self.htmlHistoryFile = "index.html"
310        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
311
312        See also: `ShowHistoryChart()`.
313        """
314
315        self.instrumentsFile = "instruments.md"
316        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
317
318        See also: `ShowInstrumentsInfo()`.
319        """
320
321        self.searchResultsFile = "search-results.md"
322        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
323
324        See also: `SearchInstruments()`.
325        """
326
327        self.pricesFile = "prices.md"
328        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
329
330        See also: `GetListOfPrices()`.
331        """
332
333        self.infoFile = "info.md"
334        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
335
336        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
337        """
338
339        self.bondsXLSXFile = "ext-bonds.xlsx"
340        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
341        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
342
343        See also: `ExtendBondsData()`.
344        """
345
346        self.calendarFile = "calendar.md"
347        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
348        
349        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
350
351        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
352        """
353
354        self.overviewFile = "overview.md"
355        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
356
357        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
358        """
359
360        self.overviewDigestFile = "overview-digest.md"
361        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
362
363        See also: `Overview()` with parameter `details="digest"`.
364        """
365
366        self.overviewPositionsFile = "overview-positions.md"
367        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
368
369        See also: `Overview()` with parameter `details="positions"`.
370        """
371
372        self.overviewOrdersFile = "overview-orders.md"
373        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
374
375        See also: `Overview()` with parameter `details="orders"`.
376        """
377
378        self.overviewAnalyticsFile = "overview-analytics.md"
379        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
380
381        See also: `Overview()` with parameter `details="analytics"`.
382        """
383
384        self.reportFile = "deals.md"
385        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
386
387        See also: `Deals()`.
388        """
389
390        self.withdrawalLimitsFile = "limits.md"
391        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
392
393        See also: `OverviewLimits()` and `RequestLimits()`.
394        """
395
396        self.userInfoFile = "user-info.md"
397        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
398
399        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
400        """
401
402        self.userAccountsFile = "accounts.md"
403        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
404
405        See also: `OverviewAccounts()`, `RequestAccounts()`.
406        """
407
408        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
409        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
410
411        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
412
413        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
414        """
415
416        self.iList = None  # init iList for raw instruments data
417        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
418        
419        See also: `Listing()`, `DumpInstruments()`.
420        """
421
422        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
423        if useCache:
424            if os.path.exists(self.iListDumpFile):
425                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
426                curTime = datetime.now(tzutc())
427
428                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
429                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
430
431                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
432
433                else:
434                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
435
436                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
437                        os.path.abspath(self.iListDumpFile),
438                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
439                    ))
440
441            else:
442                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
443                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
444
445        else:
446            self.iList = self.Listing()  # request new raw instruments data from broker server
447            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
448
449        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
450        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
451
452        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
453        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
469    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
470        """
471        Send GET or POST request to broker server and receive JSON object.
472
473        self.header: must be defining with dictionary of headers.
474        self.body: if define then used as request body. None by default.
475        self.timeout: global request timeout, 15 seconds by default.
476        :param url: url with REST request.
477        :param reqType: send "GET" or "POST" request. "GET" by default.
478        :param retry: how many times retry after first request if an 5xx server errors occurred.
479        :param pause: sleep time in seconds between retries.
480        :return: response JSON (dictionary) from broker.
481        """
482        if reqType not in ("GET", "POST"):
483            uLogger.error("You can define request type: 'GET' or 'POST'!")
484            raise Exception("Incorrect value")
485
486        if self.moreDebug:
487            uLogger.debug("Request parameters:")
488            uLogger.debug("    - REST API URL: {}".format(url))
489            uLogger.debug("    - request type: {}".format(reqType))
490            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
491            uLogger.debug("    - body:\n{}".format(self.body))
492
493        # fast hack to avoid all operations with some tickers/FIGI
494        responseJSON = {}
495        oK = True
496        for item in self.exclude:
497            if item in url:
498                if self.moreDebug:
499                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
500
501                oK = False
502                break
503
504        if oK:
505            counter = 0
506            response = None
507            errMsg = ""
508
509            while not response and counter <= retry:
510                if reqType == "GET":
511                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
512
513                if reqType == "POST":
514                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
515
516                if self.moreDebug:
517                    uLogger.debug("Response:")
518                    uLogger.debug("    - status code: {}".format(response.status_code))
519                    uLogger.debug("    - reason: {}".format(response.reason))
520                    uLogger.debug("    - body length: {}".format(len(response.text)))
521                    uLogger.debug("    - headers:\n{}".format(response.headers))
522
523                # Server returns some headers:
524                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
525                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
526                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
527                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
528                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
529                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
530                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
531                    sleep(rateLimitWait)
532
533                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
534                if 400 <= response.status_code < 500:
535                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
536                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
537                    counter = retry + 1
538
539                if 500 <= response.status_code < 600:
540                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
541                    uLogger.debug("    - not oK, {}".format(errMsg))
542                    counter += 1
543
544                    if counter <= retry:
545                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
546                        sleep(pause)
547
548            responseJSON = self._ParseJSON(rawData=response.text)
549
550            if errMsg:
551                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
552                uLogger.error("    - not oK, {}".format(errMsg))
553
554        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
587    def Listing(self) -> dict:
588        """
589        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
590
591        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
592        """
593        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
594        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
595
596        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
597        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
598        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
599
600        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
601        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
602        poolUpdater.close()
603
604        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
605        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
606        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
607
608        # calculate minimum price increment (step) for all instruments and set up instrument's type:
609        for iType in iList.keys():
610            for ticker in iList[iType]:
611                iList[iType][ticker]["type"] = iType
612
613                if "minPriceIncrement" in iList[iType][ticker].keys():
614                    iList[iType][ticker]["step"] = NanoToFloat(
615                        iList[iType][ticker]["minPriceIncrement"]["units"],
616                        iList[iType][ticker]["minPriceIncrement"]["nano"],
617                    )
618
619                else:
620                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
621
622        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
624    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
625        """
626        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
627
628        See also: `DumpInstruments()`, `Listing()`.
629
630        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
631                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
632        """
633        if self.iListDumpFile is None or not self.iListDumpFile:
634            uLogger.error("Output name of dump file must be defined!")
635            raise Exception("Filename required")
636
637        if not self.iList or forceUpdate:
638            self.iList = self.Listing()
639
640        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
641
642        # Save as XLSX with separated sheets for every type of instruments:
643        with pd.ExcelWriter(
644                path=xlsxDumpFile,
645                date_format=TKS_DATE_FORMAT,
646                datetime_format=TKS_DATE_TIME_FORMAT,
647                mode="w",
648        ) as writer:
649            for iType in TKS_INSTRUMENTS:
650                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
651                df = df[sorted(df)]  # sorted by column names
652                df = df.applymap(
653                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
654                    na_action="ignore",
655                )  # converting numbers from nano-type to float in every cell
656                df.to_excel(
657                    writer,
658                    sheet_name=iType,
659                    encoding="UTF-8",
660                    freeze_panes=(1, 1),
661                )  # saving as XLSX-file with freeze first row and column as headers
662
663        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
665    def DumpInstruments(self, forceUpdate: bool = True) -> str:
666        """
667        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
668        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
669
670        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
671
672        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
673                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
674        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
675        """
676        if self.iListDumpFile is None or not self.iListDumpFile:
677            uLogger.error("Output name of dump file must be defined!")
678            raise Exception("Filename required")
679
680        if not self.iList or forceUpdate:
681            self.iList = self.Listing()
682
683        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
684        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
685            fH.write(jsonDump)
686
687        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
688
689        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
691    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
692        """
693        Show information about one instrument defined by json data and prints it in Markdown format.
694
695        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
696
697        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
698        :param show: if `True` then also printing information about instrument and its current price.
699        :return: multilines text in Markdown format with information about one instrument.
700        """
701        splitLine = "|                                                             |                                                        |\n"
702        infoText = ""
703
704        if iJSON is not None and iJSON and isinstance(iJSON, dict):
705            info = [
706                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
707                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
708                "| Parameters                                                  | Values                                                 |\n",
709                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
710                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
711                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
712            ]
713
714            if "sector" in iJSON.keys() and iJSON["sector"]:
715                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
716
717            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
718                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
719                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
720            )))
721
722            info.extend([
723                splitLine,
724                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
725                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
726            ])
727
728            if "isin" in iJSON.keys() and iJSON["isin"]:
729                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
730
731            if "classCode" in iJSON.keys():
732                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
733
734            info.extend([
735                splitLine,
736                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
737                splitLine,
738                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
739                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
740                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
741            ])
742
743            if iJSON["figi"]:
744                self.figi = iJSON["figi"]
745                iJSON = iJSON | self.RequestTradingStatus()
746
747                info.extend([
748                    splitLine,
749                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
750                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
751                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
752                ])
753
754            info.append(splitLine)
755
756            if "type" in iJSON.keys() and iJSON["type"]:
757                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
758
759            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
760                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
761
762            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
763                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
764
765            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
766                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
767
768            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
769                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
770
771            if "focusType" in iJSON.keys() and iJSON["focusType"]:
772                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
773
774            if "assetType" in iJSON.keys() and iJSON["assetType"]:
775                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
776
777            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
778                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
779
780            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
781                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
782
783            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
784                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
785
786            if "currency" in iJSON.keys():
787                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
788
789            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
790                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
791
792            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
793                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
794
795            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
796                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
797
798            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
799                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
800
801            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
802                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
803
804            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
805                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
806
807            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
808                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
809
810            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
811                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
812
813            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
814                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
815
816            iExt = None
817            if iJSON["type"] == "Bonds":
818                info.extend([
819                    splitLine,
820                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
821                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
822                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
823                        iJSON["nominal"]["currency"],
824                    )),
825                ])
826
827                if "floatingCouponFlag" in iJSON.keys():
828                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
829
830                if "amortizationFlag" in iJSON.keys():
831                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
832
833                info.append(splitLine)
834
835                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
836                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
837
838                if iJSON["figi"]:
839                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
840
841                    info.extend([
842                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
843                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
844                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
845                    ])
846
847                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
848                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
849                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
850                        iJSON["aciValue"]["currency"]
851                    )))
852
853            if "currentPrice" in iJSON.keys():
854                info.append(splitLine)
855
856                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
857                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
858
859                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
860                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
861                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
862                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
863                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
864
865                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
866                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
867
868                info.extend([
869                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
870                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
871                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
872                    )),
873                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
874                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
875                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
876                    )),
877                    "| Changes between last deal price and last close              | {:<54} |\n".format(
878                        "{:.2f}%{}".format(
879                            iJSON["currentPrice"]["changes"],
880                            " ({}{:.2f} {})".format(
881                                "+" if bondChangesDelta > 0 else "",
882                                bondChangesDelta,
883                                aciCurrency
884                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
885                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
886                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
887                                currency
888                            ),
889                        )
890                    ),
891                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
892                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
893                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
894                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
896                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
897                    )),
898                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
899                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
900                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
901                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
902                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
903                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
904                    )),
905                ])
906
907            if "lot" in iJSON.keys():
908                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
909
910            if "step" in iJSON.keys() and iJSON["step"] != 0:
911                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
912
913            # Add bond payment calendar:
914            if iJSON["type"] == "Bonds":
915                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
916                info.extend(["\n", strCalendar])
917
918            infoText += "".join(info)
919
920            if show:
921                uLogger.info("{}".format(infoText))
922
923            else:
924                uLogger.debug("{}".format(infoText))
925
926            if self.infoFile is not None:
927                with open(self.infoFile, "w", encoding="UTF-8") as fH:
928                    fH.write(infoText)
929
930                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
931
932        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 934    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 935        """
 936        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 937
 938        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 939        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 940        :return: JSON formatted data with information about instrument.
 941        """
 942        tickerJSON = {}
 943        if self.moreDebug:
 944            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 945
 946        if not self.ticker:
 947            uLogger.warning("self.ticker variable is not be empty!")
 948
 949        else:
 950            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 951                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 952                raise Exception("Instrument not allowed")
 953
 954            if not self.iList:
 955                self.iList = self.Listing()
 956
 957            if self.ticker in self.iList["Shares"].keys():
 958                tickerJSON = self.iList["Shares"][self.ticker]
 959                if self.moreDebug:
 960                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 961
 962            elif self.ticker in self.iList["Currencies"].keys():
 963                tickerJSON = self.iList["Currencies"][self.ticker]
 964                if self.moreDebug:
 965                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 966
 967            elif self.ticker in self.iList["Bonds"].keys():
 968                tickerJSON = self.iList["Bonds"][self.ticker]
 969                if self.moreDebug:
 970                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 971
 972            elif self.ticker in self.iList["Etfs"].keys():
 973                tickerJSON = self.iList["Etfs"][self.ticker]
 974                if self.moreDebug:
 975                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 976
 977            elif self.ticker in self.iList["Futures"].keys():
 978                tickerJSON = self.iList["Futures"][self.ticker]
 979                if self.moreDebug:
 980                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 981
 982        if tickerJSON:
 983            self.figi = tickerJSON["figi"]
 984
 985            if requestPrice:
 986                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 987
 988                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 989                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 990
 991                else:
 992                    tickerJSON["currentPrice"]["changes"] = 0
 993
 994            if show:
 995                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 996
 997        else:
 998            if show:
 999                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1000
1001        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1003    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1004        """
1005        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1006
1007        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1008        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if self.moreDebug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if self.moreDebug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if self.moreDebug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if self.moreDebug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if self.moreDebug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1100        `{"buy": [{"price": 1243.8, "quantity": 193},
1101                  {"price": 1244.0, "quantity": 168},
1102                  {"price": 1244.8, "quantity": 5},
1103                  {"price": 1245.0, "quantity": 61},
1104                  {"price": 1245.4, "quantity": 60}],
1105          "sell": [{"price": 1243.6, "quantity": 8},
1106                   {"price": 1242.6, "quantity": 10},
1107                   {"price": 1242.4, "quantity": 18},
1108                   {"price": 1242.2, "quantity": 50},
1109                   {"price": 1242.0, "quantity": 113}],
1110          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1111        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1112        - sell: list of dicts with Buyers prices,
1113            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1114            - quantity: volume value by current price in lots,
1115        - limitUp: current trade session limit price, maximum,
1116        - limitDown: current trade session limit price, minimum,
1117        - lastPrice: last deal price of the instrument,
1118        - closePrice: previous trade session close price of the instrument.
1119
1120        See also: `SearchByTicker()` and `SearchByFIGI()`.
1121        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1122        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1123
1124        :param show: if `True` then print DOM to log and console.
1125        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1126                 If an error occurred then returns an empty record:
1127                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1128        """
1129        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1130
1131        if self.depth < 1:
1132            uLogger.error("Depth of Market (DOM) must be >=1!")
1133            raise Exception("Incorrect value")
1134
1135        if not (self.ticker or self.figi):
1136            uLogger.error("self.ticker or self.figi variables must be defined!")
1137            raise Exception("Ticker or FIGI required")
1138
1139        if self.ticker and not self.figi:
1140            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1141            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1142
1143        if not self.ticker and self.figi:
1144            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1145            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1146
1147        if not self.figi:
1148            uLogger.error("FIGI is not defined!")
1149            raise Exception("Ticker or FIGI required")
1150
1151        else:
1152            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1153
1154            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1155            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1156            self.body = str({"figi": self.figi, "depth": self.depth})
1157            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1158
1159            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1160                # list of dicts with sellers orders:
1161                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1162
1163                # list of dicts with buyers orders:
1164                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1165
1166                # max price of instrument at this time:
1167                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1168
1169                # min price of instrument at this time:
1170                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1171
1172                # last price of deal with instrument:
1173                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1174
1175                # last close price of instrument:
1176                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1177
1178            else:
1179                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1180                uLogger.debug("Server response: {}".format(pricesResponse))
1181
1182            if show:
1183                if prices["buy"] or prices["sell"]:
1184                    info = [
1185                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1186                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1187                            self.ticker,
1188                            self.figi,
1189                            self.depth,
1190                        ),
1191                        "-" * 60, "\n",
1192                        "             Orders of Buyers | Orders of Sellers\n",
1193                        "-" * 60, "\n",
1194                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1195                        "-" * 60, "\n",
1196                    ]
1197
1198                    if not prices["buy"]:
1199                        info.append("                              | No orders!\n")
1200                        sumBuy = 0
1201
1202                    else:
1203                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1204                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1205                        for item in maxMinSorted:
1206                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1207
1208                    if not prices["sell"]:
1209                        info.append("No orders!                    |\n")
1210                        sumSell = 0
1211
1212                    else:
1213                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1214                        for item in prices["sell"]:
1215                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1216
1217                    info.extend([
1218                        "-" * 60, "\n",
1219                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1220                        "-" * 60, "\n",
1221                    ])
1222
1223                    infoText = "".join(info)
1224
1225                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1226
1227                else:
1228                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1229
1230        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1232    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1233        """
1234        This method get and show information about all available broker instruments for current user account.
1235        If `instrumentsFile` string is not empty then also save information to this file.
1236
1237        :param show: if `True` then print results to console, if `False` - print only to file.
1238        :return: multi-lines string with all available broker instruments
1239        """
1240        if not self.iList:
1241            self.iList = self.Listing()
1242
1243        info = [
1244            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1245            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1246        ]
1247
1248        # add instruments count by type:
1249        for iType in self.iList.keys():
1250            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1251
1252        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1253        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1254
1255        # generating info tables with all instruments by type:
1256        for iType in self.iList.keys():
1257            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1258
1259            for instrument in self.iList[iType].keys():
1260                iName = self.iList[iType][instrument]["name"]  # instrument's name
1261                if len(iName) > 57:
1262                    iName = "{}...".format(iName[:54])  # right trim for a long string
1263
1264                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1265                    self.iList[iType][instrument]["ticker"],
1266                    iName,
1267                    self.iList[iType][instrument]["figi"],
1268                    self.iList[iType][instrument]["currency"],
1269                    self.iList[iType][instrument]["lot"],
1270                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1271                ))
1272
1273        infoText = "".join(info)
1274
1275        if show:
1276            uLogger.info(infoText)
1277
1278        if self.instrumentsFile:
1279            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1280                fH.write(infoText)
1281
1282            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1283
1284        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1286    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1287        """
1288        This method search and show information about instruments by part of its ticker, FIGI or name.
1289        If `searchResultsFile` string is not empty then also save information to this file.
1290
1291        :param pattern: string with part of ticker, FIGI or instrument's name.
1292        :param show: if `True` then print results to console, if `False` - return list of result only.
1293        :return: list of dictionaries with all found instruments.
1294        """
1295        if not self.iList:
1296            self.iList = self.Listing()
1297
1298        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1299        compiledPattern = re.compile(pattern, re.IGNORECASE)
1300
1301        for iType in self.iList:
1302            for instrument in self.iList[iType].values():
1303                searchResult = compiledPattern.search(" ".join(
1304                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1305                ))
1306
1307                if searchResult:
1308                    searchResults[iType][instrument["ticker"]] = instrument
1309
1310        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1311        info = [
1312            "# Search results\n\n",
1313            "* **Search pattern:** [{}]\n".format(pattern),
1314            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1315            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1316        ]
1317        infoShort = info[:]
1318
1319        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1320        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1321        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1322
1323        if resultsLen == 0:
1324            info.append("\nNo results\n")
1325            infoShort.append("\nNo results\n")
1326            uLogger.warning("No results. Try changing your search pattern.")
1327
1328        else:
1329            for iType in searchResults:
1330                iTypeValuesCount = len(searchResults[iType].values())
1331                if iTypeValuesCount > 0:
1332                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1333                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1334
1335                    for instrument in searchResults[iType].values():
1336                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1337                            instrument["type"],
1338                            instrument["ticker"],
1339                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1340                            instrument["figi"],
1341                        ))
1342
1343                    if iTypeValuesCount <= 5:
1344                        infoShort.extend(info[-iTypeValuesCount:])
1345
1346                    else:
1347                        infoShort.extend(info[-5:])
1348                        infoShort.append(skippedLine)
1349
1350        infoText = "".join(info)
1351        infoTextShort = "".join(infoShort)
1352
1353        if show:
1354            uLogger.info(infoTextShort)
1355            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1356
1357        if self.searchResultsFile:
1358            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1359                fH.write(infoText)
1360
1361            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1362
1363        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1365    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1366        """
1367        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1368
1369        :param instruments: list of strings with tickers or FIGIs.
1370        :return: list with unique instrument FIGIs only.
1371        """
1372        requestedInstruments = []
1373        for iName in instruments:
1374            if iName not in self.aliases.keys():
1375                if iName not in requestedInstruments:
1376                    requestedInstruments.append(iName)
1377
1378            else:
1379                if iName not in requestedInstruments:
1380                    if self.aliases[iName] not in requestedInstruments:
1381                        requestedInstruments.append(self.aliases[iName])
1382
1383        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1384
1385        onlyUniqueFIGIs = []
1386        for iName in requestedInstruments:
1387            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1388                continue
1389
1390            self.ticker = iName
1391            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1392
1393            if not iData:
1394                self.ticker = ""
1395                self.figi = iName
1396
1397                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1398
1399                if not iData:
1400                    self.figi = ""
1401                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1402
1403            if iData and iData["figi"] not in onlyUniqueFIGIs:
1404                onlyUniqueFIGIs.append(iData["figi"])
1405
1406        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1407
1408        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1410    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1411        """
1412        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1413        See limits: https://tinkoff.github.io/investAPI/limits/
1414        If `pricesFile` string is not empty then also save information to this file.
1415
1416        :param instruments: list of strings with tickers or FIGIs.
1417        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1418        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1419                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1420        """
1421        if instruments is None or not instruments:
1422            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1423            raise Exception("Ticker or FIGI required")
1424
1425        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1426
1427        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1428
1429        iList = []  # trying to get info and current prices about all unique instruments:
1430        for self.figi in onlyUniqueFIGIs:
1431            iData = self.SearchByFIGI(requestPrice=True)
1432            iList.append(iData)
1433
1434        self.ShowListOfPrices(iList, show)
1435
1436        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1438    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1439        """
1440        Show table contains current prices of given instruments.
1441
1442        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1443                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1444        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1445        :return: multilines text in Markdown format as a table contains current prices.
1446        """
1447        infoText = ""
1448
1449        if show or self.pricesFile:
1450            info = [
1451                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1452                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1453                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1454            ]
1455
1456            for item in iList:
1457                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1458                    item["ticker"],
1459                    item["figi"],
1460                    item["type"],
1461                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1462                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1463                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1464                    "{} / {}".format(
1465                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1466                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1467                    ),
1468                    "{} / {}".format(
1469                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1470                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1471                    ),
1472                    item["currency"],
1473                ))
1474
1475            infoText = "".join(info)
1476
1477            if show:
1478                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1479
1480            if self.pricesFile:
1481                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1482                    fH.write(infoText)
1483
1484                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1485
1486        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1488    def RequestTradingStatus(self) -> dict:
1489        """
1490        Requesting trading status for the instrument defined by `figi` variable.
1491        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1492        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1493
1494        :return: dictionary with trading status attributes. Response example:
1495                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1496                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1497        """
1498        if self.figi is None or not self.figi:
1499            uLogger.error("Variable `figi` must be defined for using this method!")
1500            raise Exception("FIGI required")
1501
1502        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1503
1504        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1505        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1506        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1507
1508        if self.moreDebug:
1509            uLogger.debug("Records about current trading status successfully received")
1510
1511        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1513    def RequestPortfolio(self) -> dict:
1514        """
1515        Requesting actual user's portfolio for current `accountId`.
1516        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending orders for current `accountId`.
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1564
1565        :return: list of dictionaries with pending orders.
1566        """
1567        if self.accountId is None or not self.accountId:
1568            uLogger.error("Variable `accountId` must be defined for using this method!")
1569            raise Exception("Account ID required")
1570
1571        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1572
1573        self.body = str({"accountId": self.accountId})
1574        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1575        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1576
1577        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1578
1579        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1581    def RequestStopOrders(self) -> list:
1582        """
1583        Requesting current actual stop orders for current `accountId`.
1584        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1585        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1586
1587        :return: list of dictionaries with stop orders.
1588        """
1589        if self.accountId is None or not self.accountId:
1590            uLogger.error("Variable `accountId` must be defined for using this method!")
1591            raise Exception("Account ID required")
1592
1593        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1594
1595        self.body = str({"accountId": self.accountId})
1596        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1597        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1598
1599        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1600
1601        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1603    def Overview(self, show: bool = False, details: str = "full") -> dict:
1604        """
1605        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1606        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1607        are defined then also save information to file.
1608
1609        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1610        many requests about the state of the portfolio, and then, based on the received data, a large number
1611        of calculation and statistics are collected.
1612
1613        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1614        :param details: how detailed should the information be? You should specify one of strings:
1615                        `full` - shows full available information about portfolio status (by default),
1616                        `positions` - shows only open positions,
1617                        `digest` - show a short digest of the portfolio status,
1618                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1619                        `orders` - shows only sections of open limits and stop orders.
1620        :return: dictionary with client's raw portfolio and some statistics.
1621        """
1622        if self.accountId is None or not self.accountId:
1623            uLogger.error("Variable `accountId` must be defined for using this method!")
1624            raise Exception("Account ID required")
1625
1626        view = {
1627            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1628                "headers": {},  # list of dictionaries, response headers without "positions" section
1629                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1630                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1631                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1632                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1633                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1634                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1635                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1636                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1637                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1638            },
1639            "stat": {  # --- some statistics calculated using "raw" sections:
1640                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1641                "availableRUB": 0.,  # available rubles (without other currencies)
1642                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1643                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1644                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1645                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1646                "sharesCostRUB": 0.,  # costs of all shares in RUB
1647                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1648                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1649                "futuresCostRUB": 0.,  # costs of all futures in RUB
1650                "Currencies": [],  # list of dictionaries of all currencies statistics
1651                "Shares": [],  # list of dictionaries of all shares statistics
1652                "Bonds": [],  # list of dictionaries of all bonds statistics
1653                "Etfs": [],  # list of dictionaries of all etfs statistics
1654                "Futures": [],  # list of dictionaries of all futures statistics
1655                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1656                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1657                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1658                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1659                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1660            },
1661            "analytics": {  # --- some analytics of portfolio:
1662                "distrByAssets": {},  # portfolio distribution by assets
1663                "distrByCompanies": {},  # portfolio distribution by companies
1664                "distrBySectors": {},  # portfolio distribution by sectors
1665                "distrByCurrencies": {},  # portfolio distribution by currencies
1666                "distrByCountries": {},  # portfolio distribution by countries
1667            }
1668        }
1669
1670        details = details.lower()
1671        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1672        if details not in availableDetails:
1673            details = "full"
1674            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1675
1676        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1677
1678        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1679        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1680        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1681        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1682
1683        # save response headers without "positions" section:
1684        for key in portfolioResponse.keys():
1685            if key != "positions":
1686                view["raw"]["headers"][key] = portfolioResponse[key]
1687
1688            else:
1689                continue
1690
1691        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1692        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1693        for item in portfolioResponse["positions"]:
1694            if item["instrumentType"] == "currency":
1695                self.figi = item["figi"]
1696                curr = self.SearchByFIGI(requestPrice=False)
1697
1698                # current price of currency in RUB:
1699                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1700                    "name": curr["name"],
1701                    "currentPrice": NanoToFloat(
1702                        item["currentPrice"]["units"],
1703                        item["currentPrice"]["nano"]
1704                    ),
1705                }
1706
1707                view["raw"]["Currencies"].append(item)
1708
1709            elif item["instrumentType"] == "share":
1710                view["raw"]["Shares"].append(item)
1711
1712            elif item["instrumentType"] == "bond":
1713                view["raw"]["Bonds"].append(item)
1714
1715            elif item["instrumentType"] == "etf":
1716                view["raw"]["Etfs"].append(item)
1717
1718            elif item["instrumentType"] == "futures":
1719                view["raw"]["Futures"].append(item)
1720
1721            else:
1722                continue
1723
1724        # how many volume of currencies (by ISO currency name) are blocked:
1725        for item in view["raw"]["positions"]["blocked"]:
1726            blocked = NanoToFloat(item["units"], item["nano"])
1727            if blocked > 0:
1728                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1729
1730        # how many volume of instruments (by FIGI) are blocked:
1731        for item in view["raw"]["positions"]["securities"]:
1732            blocked = int(item["blocked"])
1733            if blocked > 0:
1734                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1735
1736        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1737
1738        if "rub" in allBlocked.keys():
1739            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1740
1741        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1742        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1743        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1744        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1745        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1746        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1747        view["stat"]["portfolioCostRUB"] = sum([
1748            view["stat"]["allCurrenciesCostRUB"],
1749            view["stat"]["sharesCostRUB"],
1750            view["stat"]["bondsCostRUB"],
1751            view["stat"]["etfsCostRUB"],
1752            view["stat"]["futuresCostRUB"],
1753        ])
1754
1755        # --- calculating some portfolio statistics:
1756        byComp = {}  # distribution by companies
1757        bySect = {}  # distribution by sectors
1758        byCurr = {}  # distribution by currencies (include RUB)
1759        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1760        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1761
1762        for item in portfolioResponse["positions"]:
1763            self.figi = item["figi"]
1764            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1765
1766            if instrument:
1767                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1768                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1769
1770                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1771                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1772
1773                else:
1774                    blocked = 0
1775
1776                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1777                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1778                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1779                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1780                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1781                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1782                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1783                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1784                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1785                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1786                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1787                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1788
1789                statData = {
1790                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1791                    "ticker": instrument["ticker"],  # ticker by FIGI
1792                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1793                    "volume": volume,  # available volume of instrument
1794                    "lots": lots,  # volume in lots of instrument
1795                    "direction": direction,  # direction of an instrument's position: short or long
1796                    "blocked": blocked,  # blocked volume of currency or instrument
1797                    "currentPrice": curPrice,  # current instrument's price in basic asset
1798                    "average": average,  # current average position price
1799                    "cost": cost,  # current cost of all volume of instrument in basic asset
1800                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1801                    "costRUB": costRUB,  # cost of instrument in ruble
1802                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1803                    "profit": profit,  # expected profit at current moment
1804                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1805                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1806                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1807                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1808                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1809                    "step": instrument["step"],  # minimum price increment
1810                }
1811
1812                # adding distribution by unique countries:
1813                if statData["country"] not in byCountry.keys():
1814                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1815
1816                else:
1817                    byCountry[statData["country"]]["cost"] += costRUB
1818                    byCountry[statData["country"]]["percent"] += percentCostRUB
1819
1820                if item["instrumentType"] != "currency":
1821                    # adding distribution by unique companies:
1822                    if statData["name"]:
1823                        if statData["name"] not in byComp.keys():
1824                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1825
1826                        else:
1827                            byComp[statData["name"]]["cost"] += costRUB
1828                            byComp[statData["name"]]["percent"] += percentCostRUB
1829
1830                    # adding distribution by unique sectors:
1831                    if statData["sector"] not in bySect.keys():
1832                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1833
1834                    else:
1835                        bySect[statData["sector"]]["cost"] += costRUB
1836                        bySect[statData["sector"]]["percent"] += percentCostRUB
1837
1838                # adding distribution by unique currencies:
1839                if currency not in byCurr.keys():
1840                    byCurr[currency] = {
1841                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1842                        "cost": costRUB,
1843                        "percent": percentCostRUB
1844                    }
1845
1846                else:
1847                    byCurr[currency]["cost"] += costRUB
1848                    byCurr[currency]["percent"] += percentCostRUB
1849
1850                # saving statistics for every instrument:
1851                if item["instrumentType"] == "currency":
1852                    view["stat"]["Currencies"].append(statData)
1853
1854                    # update dict with free funds for trading (total - blocked) by currencies
1855                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1856                    view["stat"]["funds"][currency] = {
1857                        "total": volume,
1858                        "totalCostRUB": costRUB,  # total volume cost in rubles
1859                        "free": volume - blocked,
1860                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1861                    }
1862
1863                elif item["instrumentType"] == "share":
1864                    view["stat"]["Shares"].append(statData)
1865
1866                elif item["instrumentType"] == "bond":
1867                    view["stat"]["Bonds"].append(statData)
1868
1869                elif item["instrumentType"] == "etf":
1870                    view["stat"]["Etfs"].append(statData)
1871
1872                elif item["instrumentType"] == "Futures":
1873                    view["stat"]["Futures"].append(statData)
1874
1875                else:
1876                    continue
1877
1878        # total changes in Russian Ruble:
1879        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1880        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1881        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1882        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1883        view["stat"]["funds"]["rub"] = {
1884            "total": view["stat"]["availableRUB"],
1885            "totalCostRUB": view["stat"]["availableRUB"],
1886            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1887            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1888        }
1889
1890        # --- pending orders sector data:
1891        uniquePendingOrders = []
1892        uniquePendingOrdersFIGIs = []
1893        for item in view["raw"]["orders"]:
1894            if item["figi"] not in uniquePendingOrdersFIGIs:
1895                uniquePendingOrdersFIGIs.append(item["figi"])
1896                uniquePendingOrders.append(item)
1897
1898        for item in uniquePendingOrders:
1899            self.figi = item["figi"]
1900            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1901
1902            if instrument:
1903                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1904                orderType = TKS_ORDER_TYPES[item["orderType"]]
1905                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1906                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1907
1908                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1909                if item["direction"] == "ORDER_DIRECTION_BUY":
1910                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1911
1912                else:
1913                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1914
1915                # requested price for order execution:
1916                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1917
1918                # necessary changes in percent to reach target from current price:
1919                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1920
1921                view["stat"]["orders"].append({
1922                    "orderID": item["orderId"],  # orderId number parameter of current order
1923                    "figi": item["figi"],  # FIGI identification
1924                    "ticker": instrument["ticker"],  # ticker name by FIGI
1925                    "lotsRequested": item["lotsRequested"],  # requested lots value
1926                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1927                    "currentPrice": lastPrice,  # current instrument's price for defined action
1928                    "targetPrice": target,  # requested price for order execution in base currency
1929                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1930                    "percentChanges": changes,  # changes in percent to target from current price
1931                    "currency": item["currency"],  # instrument's currency name
1932                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1933                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1934                    "status": orderState,  # order status from TKS_ORDER_STATES
1935                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1936                })
1937
1938        # --- stop orders sector data:
1939        uniqueStopOrders = []
1940        uniqueStopOrdersFIGIs = []
1941        for item in view["raw"]["stopOrders"]:
1942            if item["figi"] not in uniqueStopOrdersFIGIs:
1943                uniqueStopOrdersFIGIs.append(item["figi"])
1944                uniqueStopOrders.append(item)
1945
1946        for item in uniqueStopOrders:
1947            self.figi = item["figi"]
1948            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1949
1950            if instrument:
1951                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1952                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1953                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1954
1955                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1956                if "expirationTime" in item.keys():
1957                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1958                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1959
1960                else:
1961                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1962                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1963
1964                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1965                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1966                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1967
1968                else:
1969                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1970
1971                # requested price when stop-order executed:
1972                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1973
1974                # price for limit-order, set up when stop-order executed:
1975                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1976
1977                # necessary changes in percent to reach target from current price:
1978                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1979
1980                view["stat"]["stopOrders"].append({
1981                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1982                    "figi": item["figi"],  # FIGI identification
1983                    "ticker": instrument["ticker"],  # ticker name by FIGI
1984                    "lotsRequested": item["lotsRequested"],  # requested lots value
1985                    "currentPrice": lastPrice,  # current instrument's price for defined action
1986                    "targetPrice": target,  # requested price for stop-order execution in base currency
1987                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1988                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1989                    "percentChanges": changes,  # changes in percent to target from current price
1990                    "currency": item["currency"],  # instrument's currency name
1991                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1992                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1993                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1994                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1995                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1996                })
1997
1998        # --- calculating data for analytics section:
1999        # portfolio distribution by assets:
2000        view["analytics"]["distrByAssets"] = {
2001            "Ruble": {
2002                "uniques": 1,
2003                "cost": view["stat"]["availableRUB"],
2004                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006            "Currencies": {
2007                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2008                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2009                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2010            },
2011            "Shares": {
2012                "uniques": len(view["stat"]["Shares"]),
2013                "cost": view["stat"]["sharesCostRUB"],
2014                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2015            },
2016            "Bonds": {
2017                "uniques": len(view["stat"]["Bonds"]),
2018                "cost": view["stat"]["bondsCostRUB"],
2019                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2020            },
2021            "Etfs": {
2022                "uniques": len(view["stat"]["Etfs"]),
2023                "cost": view["stat"]["etfsCostRUB"],
2024                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2025            },
2026            "Futures": {
2027                "uniques": len(view["stat"]["Futures"]),
2028                "cost": view["stat"]["futuresCostRUB"],
2029                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2030            },
2031        }
2032
2033        # portfolio distribution by companies:
2034        view["analytics"]["distrByCompanies"]["All money cash"] = {
2035            "ticker": "",
2036            "cost": view["stat"]["allCurrenciesCostRUB"],
2037            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038        }
2039        view["analytics"]["distrByCompanies"].update(byComp)
2040
2041        # portfolio distribution by sectors:
2042        view["analytics"]["distrBySectors"]["All money cash"] = {
2043            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2044            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2045        }
2046        view["analytics"]["distrBySectors"].update(bySect)
2047
2048        # portfolio distribution by currencies:
2049        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2050            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2051            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2052
2053        view["analytics"]["distrByCurrencies"].update(byCurr)
2054        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2055        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2056
2057        # portfolio distribution by countries:
2058        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2059            uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2060            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2061
2062        view["analytics"]["distrByCountries"].update(byCountry)
2063        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2064        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2065
2066        # --- Prepare text statistics overview in human-readable:
2067        if show:
2068            # Whatever the value `details`, header not changes:
2069            info = [
2070                "# Client's portfolio\n\n",
2071                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2072                "* **Account ID:** [{}]\n".format(self.accountId),
2073            ]
2074
2075            if details in ["full", "positions", "digest"]:
2076                info.extend([
2077                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2078                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2079                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2080                        view["stat"]["totalChangesRUB"],
2081                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2082                        view["stat"]["totalChangesPercentRUB"],
2083                    ),
2084                ])
2085
2086            if details in ["full", "positions"]:
2087                info.extend([
2088                    "## Open positions\n\n",
2089                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2090                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2091                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2092                        "{:.2f} ({:.2f}) rub".format(
2093                            view["stat"]["availableRUB"],
2094                            view["stat"]["blockedRUB"],
2095                        )
2096                    )
2097                ])
2098
2099                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2100                    return [
2101                        "|                             |                                 |          |              |              |                     |                              |\n",
2102                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2103                            noTradeStr if noTradeStr else typeStr,
2104                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2105                        ),
2106                    ]
2107
2108                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2109                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2110                        "{} [{}]".format(data["ticker"], data["figi"]),
2111                        "{:.2f} ({:.2f}) {}".format(
2112                            data["volume"],
2113                            data["blocked"],
2114                            data["currency"],
2115                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2116                            data["volume"],
2117                            data["blocked"],
2118                        ),
2119                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2120                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2121                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2122                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2123                        "{}{:.2f} {} ({}{:.2f}%)".format(
2124                            "+" if data["profit"] > 0 else "",
2125                            data["profit"], data["baseCurrencyName"],
2126                            "+" if data["percentProfit"] > 0 else "",
2127                            data["percentProfit"],
2128                        ),
2129                    )
2130
2131                # --- Show currencies section:
2132                if view["stat"]["Currencies"]:
2133                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2134                    for item in view["stat"]["Currencies"]:
2135                        info.append(_InfoStr(item, showCurrencyName=True))
2136
2137                else:
2138                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2139
2140                # --- Show shares section:
2141                if view["stat"]["Shares"]:
2142                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2143
2144                    for item in view["stat"]["Shares"]:
2145                        info.append(_InfoStr(item))
2146
2147                else:
2148                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2149
2150                # --- Show bonds section:
2151                if view["stat"]["Bonds"]:
2152                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2153
2154                    for item in view["stat"]["Bonds"]:
2155                        info.append(_InfoStr(item))
2156
2157                else:
2158                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2159
2160                # --- Show etfs section:
2161                if view["stat"]["Etfs"]:
2162                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2163
2164                    for item in view["stat"]["Etfs"]:
2165                        info.append(_InfoStr(item))
2166
2167                else:
2168                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2169
2170                # --- Show futures section:
2171                if view["stat"]["Futures"]:
2172                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2173
2174                    for item in view["stat"]["Futures"]:
2175                        info.append(_InfoStr(item))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2179
2180            if details in ["full", "orders"]:
2181                # --- Show pending orders section:
2182                if view["stat"]["orders"]:
2183                    info.extend([
2184                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2185                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2186                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2187                    ])
2188
2189                    for item in view["stat"]["orders"]:
2190                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2191                            "{} [{}]".format(item["ticker"], item["figi"]),
2192                            item["orderID"],
2193                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2194                            "{} {} ({}{:.2f}%)".format(
2195                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2196                                item["baseCurrencyName"],
2197                                "+" if item["percentChanges"] > 0 else "",
2198                                float(item["percentChanges"]),
2199                            ),
2200                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2201                            item["action"],
2202                            item["type"],
2203                            item["date"],
2204                        ))
2205
2206                else:
2207                    info.append("\n## Total pending limit-orders: 0\n")
2208
2209                # --- Show stop orders section:
2210                if view["stat"]["stopOrders"]:
2211                    info.extend([
2212                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2213                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2214                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2215                    ])
2216
2217                    for item in view["stat"]["stopOrders"]:
2218                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2219                            "{} [{}]".format(item["ticker"], item["figi"]),
2220                            item["orderID"],
2221                            item["lotsRequested"],
2222                            "{} {} ({}{:.2f}%)".format(
2223                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2224                                item["baseCurrencyName"],
2225                                "+" if item["percentChanges"] > 0 else "",
2226                                float(item["percentChanges"]),
2227                            ),
2228                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2229                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2230                            item["action"],
2231                            item["type"],
2232                            item["expType"],
2233                            item["createDate"],
2234                            item["expDate"],
2235                        ))
2236
2237                else:
2238                    info.append("\n## Total stop-orders: 0\n")
2239
2240            if details in ["full", "analytics"]:
2241                # -- Show analytics section:
2242                if view["stat"]["portfolioCostRUB"] > 0:
2243                    info.extend([
2244                        "\n# Analytics\n"
2245                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2246                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2247                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2248                            view["stat"]["totalChangesRUB"],
2249                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2250                            view["stat"]["totalChangesPercentRUB"],
2251                        ),
2252                        "\n## Portfolio distribution by assets\n"
2253                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2254                        "|------------|---------|---------|--------------------|\n",
2255                    ])
2256
2257                    for key in view["analytics"]["distrByAssets"].keys():
2258                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2259                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2260                                key,
2261                                view["analytics"]["distrByAssets"][key]["uniques"],
2262                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2263                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2264                            ))
2265
2266                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2267                    info.extend([
2268                        "\n## Portfolio distribution by companies\n"
2269                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2270                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2271                    ])
2272
2273                    for company in view["analytics"]["distrByCompanies"].keys():
2274                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2275                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2276                            info.append("| {} | {:<7} | {:<18} |\n".format(
2277                                "{}{}{}".format(
2278                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2279                                    company,
2280                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2281                                ),
2282                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2283                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2284                            ))
2285
2286                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2287                    info.extend([
2288                        "\n## Portfolio distribution by sectors\n"
2289                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2290                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2291                    ])
2292
2293                    for sector in view["analytics"]["distrBySectors"].keys():
2294                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2295                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2296                                sector,
2297                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2298                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2299                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2300                            ))
2301
2302                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2303                    info.extend([
2304                        "\n## Portfolio distribution by currencies\n"
2305                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2306                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2307                    ])
2308
2309                    for curr in view["analytics"]["distrByCurrencies"].keys():
2310                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2311                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2312                            info.append("| {} | {:<7} | {:<18} |\n".format(
2313                                "[{}] {}{}".format(
2314                                    curr,
2315                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2316                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2317                                ),
2318                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2319                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2320                            ))
2321
2322                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2323                    info.extend([
2324                        "\n## Portfolio distribution by countries\n"
2325                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2326                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2327                    ])
2328
2329                    for country in view["analytics"]["distrByCountries"].keys():
2330                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2331                            nameLen = len(country)
2332                            info.append("| {} | {:<7} | {:<18} |\n".format(
2333                                "{}{}".format(
2334                                    country,
2335                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2336                                ),
2337                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2338                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2339                            ))
2340
2341            infoText = "".join(info)
2342
2343            uLogger.info(infoText)
2344
2345            if details == "full" and self.overviewFile:
2346                filename = self.overviewFile
2347
2348            elif details == "digest" and self.overviewDigestFile:
2349                filename = self.overviewDigestFile
2350
2351            elif details == "positions" and self.overviewPositionsFile:
2352                filename = self.overviewPositionsFile
2353
2354            elif details == "orders" and self.overviewOrdersFile:
2355                filename = self.overviewOrdersFile
2356
2357            elif details == "analytics" and self.overviewAnalyticsFile:
2358                filename = self.overviewAnalyticsFile
2359
2360            else:
2361                filename = ""
2362
2363            if filename:
2364                with open(filename, "w", encoding="UTF-8") as fH:
2365                    fH.write(infoText)
2366
2367                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2368
2369        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2371    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2372        """
2373        Returns history operations between two given dates for current `accountId`.
2374        If `reportFile` string is not empty then also save human-readable report.
2375        Shows some statistical data of closed positions.
2376
2377        :param start: see docstring in `GetDatesAsString()` method
2378        :param end: see docstring in `GetDatesAsString()` method
2379        :param show: if `True` then also prints all records to the console.
2380        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2381        :return: original list of dictionaries with history of deals records from API ("operations" key):
2382                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2383                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2384        """
2385        if self.accountId is None or not self.accountId:
2386            uLogger.error("Variable `accountId` must be defined for using this method!")
2387            raise Exception("Account ID required")
2388
2389        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2390
2391        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2392
2393        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2394        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2395        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2396        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2397        customStat = {}  # custom statistics in additional to responseJSON
2398
2399        # --- output report in human-readable format:
2400        if show or self.reportFile:
2401            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2402            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2403            nextDay = ""
2404
2405            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2406
2407            if len(ops) > 0:
2408                customStat = {
2409                    "opsCount": 0,  # total operations count
2410                    "buyCount": 0,  # buy operations
2411                    "sellCount": 0,  # sell operations
2412                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2413                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2414                    "payIn": {"rub": 0.},  # Deposit brokerage account
2415                    "payOut": {"rub": 0.},  # Withdrawals
2416                    "divs": {"rub": 0.},  # Dividends income
2417                    "coupons": {"rub": 0.},  # Coupon's income
2418                    "brokerCom": {"rub": 0.},  # Service commissions
2419                    "serviceCom": {"rub": 0.},  # Service commissions
2420                    "marginCom": {"rub": 0.},  # Margin commissions
2421                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2422                }
2423
2424                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2425                for item in ops:
2426                    if item["state"] == "OPERATION_STATE_EXECUTED":
2427                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2428
2429                        # count buy operations:
2430                        if "_BUY" in item["operationType"]:
2431                            customStat["buyCount"] += 1
2432
2433                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2434                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2435
2436                            else:
2437                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2438
2439                        # count sell operations:
2440                        elif "_SELL" in item["operationType"]:
2441                            customStat["sellCount"] += 1
2442
2443                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2444                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2445
2446                            else:
2447                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2448
2449                        # count incoming operations:
2450                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2451                            if item["payment"]["currency"] in customStat["payIn"].keys():
2452                                customStat["payIn"][item["payment"]["currency"]] += payment
2453
2454                            else:
2455                                customStat["payIn"][item["payment"]["currency"]] = payment
2456
2457                        # count withdrawals operations:
2458                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2459                            if item["payment"]["currency"] in customStat["payOut"].keys():
2460                                customStat["payOut"][item["payment"]["currency"]] += payment
2461
2462                            else:
2463                                customStat["payOut"][item["payment"]["currency"]] = payment
2464
2465                        # count dividends income:
2466                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2467                            if item["payment"]["currency"] in customStat["divs"].keys():
2468                                customStat["divs"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["divs"][item["payment"]["currency"]] = payment
2472
2473                        # count coupon's income:
2474                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2475                            if item["payment"]["currency"] in customStat["coupons"].keys():
2476                                customStat["coupons"][item["payment"]["currency"]] += payment
2477
2478                            else:
2479                                customStat["coupons"][item["payment"]["currency"]] = payment
2480
2481                        # count broker commissions:
2482                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2483                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2484                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2485
2486                            else:
2487                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2488
2489                        # count service commissions:
2490                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2491                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2492                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2493
2494                            else:
2495                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2496
2497                        # count margin commissions:
2498                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2499                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2500                                customStat["marginCom"][item["payment"]["currency"]] += payment
2501
2502                            else:
2503                                customStat["marginCom"][item["payment"]["currency"]] = payment
2504
2505                        # count withholding taxes:
2506                        elif "_TAX" in item["operationType"]:
2507                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2508                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2509
2510                            else:
2511                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2512
2513                        else:
2514                            continue
2515
2516                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2517
2518                # --- view "Actions" lines:
2519                info.extend([
2520                    "| Report sections            |                               |                              |                      |                        |\n",
2521                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2522                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2523                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2524                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2525                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2526                    ),
2527                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2528                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2529                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2530                    ),
2531                ])
2532
2533                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2534                for key in opsKeys:
2535                    if key == "rub":
2536                        continue
2537
2538                    info.extend([
2539                        "|                            |                               | {:<28} |                      |                        |\n".format(
2540                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2541                        ),
2542                        "|                            |                               | {:<28} |                      |                        |\n".format(
2543                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2544                        ),
2545                    ])
2546
2547                info.append(splitLine1)
2548
2549                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2550                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2551                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2552                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2553                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2554                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2555                    )
2556
2557                # --- view "Payments" lines:
2558                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2559                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2560
2561                for key in paymentsKeys:
2562                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2563
2564                info.append(splitLine1)
2565
2566                # --- view "Commissions and taxes" lines:
2567                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2568                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2569
2570                for key in comKeys:
2571                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2572
2573                info.append(splitLine1)
2574
2575                info.extend([
2576                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2577                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2578                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2579                ])
2580
2581            else:
2582                info.append("Broker returned no operations during this period\n")
2583
2584            # --- view "Operations" section:
2585            for item in ops:
2586                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2587                    continue
2588
2589                else:
2590                    self.figi = item["figi"] if item["figi"] else ""
2591                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2592                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2593
2594                    # group of deals during one day:
2595                    if nextDay and item["date"].split("T")[0] != nextDay:
2596                        info.append(splitLine2)
2597                        nextDay = ""
2598
2599                    else:
2600                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2601
2602                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2603                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2604                        self.figi if self.figi else "—",
2605                        instrument["ticker"] if instrument else "—",
2606                        instrument["type"] if instrument else "—",
2607                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2608                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2609                        TKS_OPERATION_STATES[item["state"]],
2610                        TKS_OPERATION_TYPES[item["operationType"]],
2611                    ))
2612
2613            infoText = "".join(info)
2614
2615            if show:
2616                if self.moreDebug:
2617                    uLogger.debug("Records about history of a client's operations successfully received")
2618
2619                uLogger.info(infoText)
2620
2621            if self.reportFile:
2622                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2623                    fH.write(infoText)
2624
2625                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2626
2627        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2629    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2630        """
2631        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2632
2633        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2634        Warning! Broker server used ISO UTC time by default.
2635
2636        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2637        Also, `historyFile` used to update history with `onlyMissing` parameter.
2638
2639        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2640
2641        :param start: see docstring in `GetDatesAsString()` method.
2642        :param end: see docstring in `GetDatesAsString()` method.
2643        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2644                         `"hour"`, `"day"`. Default: `"hour"`.
2645        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2646                            False by default. Warning! History appends only from last candle to current time
2647                            with always update last candle!
2648        :param csvSep: separator if csv-file is used, `,` by default.
2649        :param show: if `True` then also prints Pandas DataFrame to the console.
2650        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2651                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2652        """
2653        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2654        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2655        history = None  # empty pandas object for history
2656
2657        if interval not in TKS_CANDLE_INTERVALS.keys():
2658            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2659            raise Exception("Incorrect value")
2660
2661        if not (self.ticker or self.figi):
2662            uLogger.error("Ticker or FIGI must be defined!")
2663            raise Exception("Ticker or FIGI required")
2664
2665        if self.ticker and not self.figi:
2666            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2667            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2668
2669        if self.figi and not self.ticker:
2670            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2671            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2672
2673        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2674        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2675        if interval.lower() != "day":
2676            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2677
2678        delta = dtEnd - dtStart  # current UTC time minus last time in file
2679        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2680
2681        # calculate history length in candles:
2682        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2683        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2684            length += 1  # to avoid fraction time
2685
2686        # calculate data blocks count:
2687        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2688
2689        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2690        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2691        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2692        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2693        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2694
2695        tempOld = None  # pandas object for old history, if --only-missing key present
2696        lastTime = None  # datetime object of last old candle in file
2697
2698        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2699            uLogger.debug("--only-missing key present, add only last missing candles...")
2700            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2701
2702            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2703
2704            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2705            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2706            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2707            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2708
2709            # get last datetime object from last string in file or minus 1 delta if file is empty:
2710            if len(tempOld) > 0:
2711                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2712
2713            else:
2714                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2715
2716            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2717
2718        responseJSONs = []  # raw history blocks of data
2719
2720        blockEnd = dtEnd
2721        for item in range(blocks):
2722            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2723            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2724
2725            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2726                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2727            ))
2728
2729            if blockStart == blockEnd:
2730                uLogger.debug("Skipped this zero-length block...")
2731
2732            else:
2733                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2734                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2735                self.body = str({
2736                    "figi": self.figi,
2737                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2738                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2739                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2740                })
2741                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2742
2743                if "code" in responseJSON.keys():
2744                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2745
2746                else:
2747                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2748                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2749
2750                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2751
2752            blockEnd = blockStart
2753
2754        printCount = len(responseJSONs)  # candles to show in console
2755        if responseJSONs:
2756            tempHistory = pd.DataFrame(
2757                data={
2758                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2759                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2760                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2761                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2762                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2763                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2764                    "volume": [int(item["volume"]) for item in responseJSONs],
2765                },
2766                index=range(len(responseJSONs)),
2767                columns=["date", "time", "open", "high", "low", "close", "volume"],
2768            )
2769            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2770            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2771
2772            # append only newest candles to old history if --only-missing key present:
2773            if onlyMissing and tempOld is not None and lastTime is not None:
2774                index = 0  # find start index in tempHistory data:
2775
2776                for i, item in tempHistory.iterrows():
2777                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2778
2779                    if curTime == lastTime:
2780                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2781                        index = i
2782                        printCount = index + 1
2783                        break
2784
2785                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2786
2787            else:
2788                history = tempHistory  # if no `--only-missing` key then load full data from server
2789
2790            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2791
2792        if history is not None and not history.empty:
2793            if show:
2794                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2795                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2796                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2797                ))
2798
2799        else:
2800            uLogger.warning("Received an empty candles history!")
2801
2802        if self.historyFile is not None:
2803            if history is not None and not history.empty:
2804                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2805                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2806
2807            else:
2808                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2809
2810        else:
2811            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2812
2813        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2815    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2816        """
2817        Load candles history from csv-file and return Pandas DataFrame object.
2818
2819        See also: `History()` and `ShowHistoryChart()` methods.
2820
2821        :param filePath: path to csv-file to open.
2822        """
2823        loadedHistory = None  # init candles data object
2824
2825        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2826
2827        if os.path.exists(filePath):
2828            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2829
2830            tfStr = self.priceModel.FormattedDelta(
2831                self.priceModel.timeframe,
2832                "{days} days {hours}h {minutes}m {seconds}s",
2833            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2834                self.priceModel.timeframe,
2835                "{hours}h {minutes}m {seconds}s",
2836            )
2837
2838            if loadedHistory is not None and not loadedHistory.empty:
2839                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2840                    len(loadedHistory),
2841                    tfStr,
2842                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2843                )
2844
2845            else:
2846                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2847
2848        else:
2849            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2850
2851        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2853    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2854        """
2855        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2856
2857        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2858        Default: `index.html` (both for interact and non-interact candlesticks chart).
2859
2860        See also: `History()` and `LoadHistory()` methods.
2861
2862        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2863        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2864                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2865                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2866                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2867        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2868                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2869        """
2870        if isinstance(candles, str):
2871            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2872            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2873
2874        elif isinstance(candles, pd.DataFrame):
2875            self.priceModel.prices = candles  # set candles chain from variable
2876            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2877
2878            if "datetime" not in candles.columns:
2879                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2880
2881        else:
2882            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2883            raise Exception("Incorrect value")
2884
2885        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2886
2887        if interact:
2888            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2889
2890            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2891
2892        else:
2893            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2894
2895            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2896
2897        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2899    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2900        """
2901        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2902        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2903
2904        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2905
2906        :param operation: string "Buy" or "Sell".
2907        :param lots: volume, integer count of lots >= 1.
2908        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2909        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2910        :param expDate: string "Undefined" by default or local date in future,
2911                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2912        :return: JSON with response from broker server.
2913        """
2914        if self.accountId is None or not self.accountId:
2915            uLogger.error("Variable `accountId` must be defined for using this method!")
2916            raise Exception("Account ID required")
2917
2918        if operation is None or not operation or operation not in ("Buy", "Sell"):
2919            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2920            raise Exception("Incorrect value")
2921
2922        if lots is None or lots < 1:
2923            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2924            lots = 1
2925
2926        if tp is None or tp < 0:
2927            tp = 0
2928
2929        if sl is None or sl < 0:
2930            sl = 0
2931
2932        if expDate is None or not expDate:
2933            expDate = "Undefined"
2934
2935        if not (self.ticker or self.figi):
2936            uLogger.error("Ticker or FIGI must be defined!")
2937            raise Exception("Ticker or FIGI required")
2938
2939        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2940        self.ticker = instrument["ticker"]
2941        self.figi = instrument["figi"]
2942
2943        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2944
2945        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2946        self.body = str({
2947            "figi": self.figi,
2948            "quantity": str(lots),
2949            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2950            "accountId": str(self.accountId),
2951            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2952        })
2953        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2954
2955        if "orderId" in response.keys():
2956            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2957                operation, response["orderId"],
2958                self.ticker, self.figi, lots,
2959                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2960                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2961                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2962            ))
2963
2964        else:
2965            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2966
2967        if tp > 0:
2968            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2969
2970        if sl > 0:
2971            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2972
2973        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2975    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2976        """
2977        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2978        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2979
2980        See also: `Order()` and `Trade()` docstrings.
2981
2982        :param lots: volume, integer count of lots >= 1.
2983        :param tp: float > 0, take profit price of stop-order.
2984        :param sl: float > 0, stop loss price of stop-order.
2985        :param expDate: it's a local date in future.
2986                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2987        :return: JSON with response from broker server.
2988        """
2989        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2991    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2992        """
2993        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2994        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2995
2996        See also: `Order()` and `Trade()` docstrings.
2997
2998        :param lots: volume, integer count of lots >= 1.
2999        :param tp: float > 0, take profit price of stop-order.
3000        :param sl: float > 0, stop loss price of stop-order.
3001        :param expDate: it's a local date in the future.
3002                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3003        :return: JSON with response from broker server.
3004        """
3005        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3007    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3008        """
3009        Close position of given instruments.
3010
3011        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3012        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3013                         This avoids unnecessary downloading data from the server.
3014        """
3015        if instruments is None or not instruments:
3016            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3017            raise Exception("Ticker or FIGI required")
3018
3019        if isinstance(instruments, str):
3020            instruments = [instruments]
3021
3022        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3023        if uniqueInstruments:
3024            if portfolio is None or not portfolio:
3025                portfolio = self.Overview(show=False)
3026
3027            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3028            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3029
3030            for self.figi in uniqueInstruments:
3031                if self.figi not in allOpened:
3032                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3033                    continue
3034
3035                # search open trade info about instrument by ticker:
3036                instrument = {}
3037                for iType in TKS_INSTRUMENTS:
3038                    if instrument:
3039                        break
3040
3041                    for item in portfolio["stat"][iType]:
3042                        if item["figi"] == self.figi:
3043                            instrument = item
3044                            break
3045
3046                if instrument:
3047                    self.ticker = instrument["ticker"]
3048                    self.figi = instrument["figi"]
3049
3050                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3051                        self.ticker,
3052                        self.figi,
3053                        int(instrument["volume"]),
3054                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3055                    ))
3056
3057                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3058
3059                    if tradeLots > 0:
3060                        if instrument["blocked"] > 0:
3061                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3062                                instrument["blocked"],
3063                                self.ticker,
3064                                tradeLots,
3065                            ))
3066
3067                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3068                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3069
3070                    else:
3071                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3073    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3074        """
3075        Close all positions of given instruments with defined type.
3076
3077        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3078        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3079                         This avoids unnecessary downloading data from the server.
3080        """
3081        if iType not in TKS_INSTRUMENTS:
3082            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3083
3084        else:
3085            if portfolio is None or not portfolio:
3086                portfolio = self.Overview(show=False)
3087
3088            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3089            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3090
3091            if tickers and portfolio:
3092                self.CloseTrades(tickers, portfolio)
3093
3094            else:
3095                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3097    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3098        """
3099        Universal method to create market or limit orders with all available parameters for current `accountId`.
3100        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3101
3102        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3103        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3104
3105        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3106        then broker immediately open market order as you can do simple --buy or --sell operations!
3107
3108        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3109        When current price will go up or down to target price value then broker opens a limit order.
3110        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3111
3112        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3113
3114        :param operation: string "Buy" or "Sell".
3115        :param orderType: string "Limit" or "Stop".
3116        :param lots: volume, integer count of lots >= 1.
3117        :param targetPrice: target price > 0. This is open trade price for limit order.
3118        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3119                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3120        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3121                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3122                         Stop loss order always executed by market price.
3123        :param expDate: string "Undefined" by default or local date in future.
3124                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3125                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3126                        A limit order has no expiration date, it lasts until the end of the trading day.
3127        :return: JSON with response from broker server.
3128        """
3129        if self.accountId is None or not self.accountId:
3130            uLogger.error("Variable `accountId` must be defined for using this method!")
3131            raise Exception("Account ID required")
3132
3133        if operation is None or not operation or operation not in ("Buy", "Sell"):
3134            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3135            raise Exception("Incorrect value")
3136
3137        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3138            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3139            raise Exception("Incorrect value")
3140
3141        if lots is None or lots < 1:
3142            uLogger.error("You must define trade volume > 0: integer count of lots!")
3143            raise Exception("Incorrect value")
3144
3145        if targetPrice is None or targetPrice <= 0:
3146            uLogger.error("Target price for limit-order must be greater than 0!")
3147            raise Exception("Incorrect value")
3148
3149        if limitPrice is None or limitPrice <= 0:
3150            limitPrice = targetPrice
3151
3152        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3153            stopType = "Limit"
3154
3155        if expDate is None or not expDate:
3156            expDate = "Undefined"
3157
3158        if not (self.ticker or self.figi):
3159            uLogger.error("Tocker or FIGI must be defined!")
3160            raise Exception("Ticker or FIGI required")
3161
3162        response = {}
3163        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3164        self.ticker = instrument["ticker"]
3165        self.figi = instrument["figi"]
3166
3167        if orderType == "Limit":
3168            uLogger.debug(
3169                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3170                    self.ticker, self.figi,
3171                    operation, lots, targetPrice, instrument["currency"],
3172                ))
3173
3174            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3175            self.body = str({
3176                "figi": self.figi,
3177                "quantity": str(lots),
3178                "price": FloatToNano(targetPrice),
3179                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3180                "accountId": str(self.accountId),
3181                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3182            })
3183            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3184
3185            if "orderId" in response.keys():
3186                uLogger.info(
3187                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3188                        response["orderId"],
3189                        self.ticker, self.figi,
3190                        operation, lots, targetPrice, instrument["currency"],
3191                    ))
3192
3193                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3194                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3195                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3196                            targetPrice, instrument["currency"],
3197                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3198                        ))
3199
3200                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3201                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3202                            targetPrice, instrument["currency"],
3203                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3204                        ))
3205
3206            else:
3207                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3208
3209        if orderType == "Stop":
3210            uLogger.debug(
3211                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3212                    self.ticker, self.figi,
3213                    operation, lots,
3214                    targetPrice, instrument["currency"],
3215                    limitPrice, instrument["currency"],
3216                    stopType, expDate,
3217                ))
3218
3219            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3220            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3221            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3222
3223            body = {
3224                "figi": self.figi,
3225                "quantity": str(lots),
3226                "price": FloatToNano(limitPrice),
3227                "stopPrice": FloatToNano(targetPrice),
3228                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3229                "accountId": str(self.accountId),
3230                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3231                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3232            }
3233
3234            if expDateUTC:
3235                body["expireDate"] = expDateUTC
3236
3237            self.body = str(body)
3238            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3239
3240            if "stopOrderId" in response.keys():
3241                uLogger.info(
3242                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3243                        response["stopOrderId"],
3244                        self.ticker, self.figi,
3245                        operation, lots,
3246                        targetPrice, instrument["currency"],
3247                        limitPrice, instrument["currency"],
3248                        TKS_STOP_ORDER_TYPES[stopOrderType],
3249                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3250                    ))
3251
3252                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3253                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3254                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3255                            targetPrice, instrument["currency"],
3256                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3257                        ))
3258
3259                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3260                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3261                            targetPrice, instrument["currency"],
3262                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3263                        ))
3264
3265            else:
3266                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3267
3268        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3270    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3271        """
3272        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3273        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3274        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3275        See also: `Order()` docstring.
3276
3277        :param lots: volume, integer count of lots >= 1.
3278        :param targetPrice: target price > 0. This is open trade price for limit order.
3279        :return: JSON with response from broker server.
3280        """
3281        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3283    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3284        """
3285        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3286        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3287        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3288        target price value then broker opens a limit order. See also: `Order()` docstring.
3289
3290        :param lots: volume, integer count of lots >= 1.
3291        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3292        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3293                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3294        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3295                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3296        :param expDate: string "Undefined" by default or local date in future.
3297                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3298                        This date is converting to UTC format for server.
3299        :return: JSON with response from broker server.
3300        """
3301        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3303    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3304        """
3305        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3306        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3307        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3308        See also: `Order()` docstring.
3309
3310        :param lots: volume, integer count of lots >= 1.
3311        :param targetPrice: target price > 0. This is open trade price for limit order.
3312        :return: JSON with response from broker server.
3313        """
3314        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3316    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3317        """
3318        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3319        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3320        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3321        target price value then broker opens a limit order. See also: `Order()` docstring.
3322
3323        :param lots: volume, integer count of lots >= 1.
3324        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3325        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3326                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3327        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3328                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3329        :param expDate: string "Undefined" by default or local date in future.
3330                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3331                        This date is converting to UTC format for server.
3332        :return: JSON with response from broker server.
3333        """
3334        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3336    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3337        """
3338        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3339
3340        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3341        :param allOrdersIDs: pre-received lists of all active pending orders.
3342                             This avoids unnecessary downloading data from the server.
3343        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3344        """
3345        if self.accountId is None or not self.accountId:
3346            uLogger.error("Variable `accountId` must be defined for using this method!")
3347            raise Exception("Account ID required")
3348
3349        if orderIDs:
3350            if allOrdersIDs is None or not allOrdersIDs:
3351                rawOrders = self.RequestPendingOrders()
3352                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3353
3354            if allStopOrdersIDs is None or not allStopOrdersIDs:
3355                rawStopOrders = self.RequestStopOrders()
3356                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3357
3358            for orderID in orderIDs:
3359                idInPendingOrders = orderID in allOrdersIDs
3360                idInStopOrders = orderID in allStopOrdersIDs
3361
3362                if not (idInPendingOrders or idInStopOrders):
3363                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3364                    continue
3365
3366                else:
3367                    if idInPendingOrders:
3368                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3369
3370                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3371                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3372                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3373                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3374
3375                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3376                            if self.moreDebug:
3377                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3378
3379                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3380
3381                        else:
3382                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3383
3384                    elif idInStopOrders:
3385                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3386
3387                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3388                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3389                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3390                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3391
3392                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3393                            if self.moreDebug:
3394                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3395
3396                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3397
3398                        else:
3399                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3400
3401                    else:
3402                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3404    def CloseAllOrders(self) -> None:
3405        """
3406        Gets a list of open pending and stop orders and cancel it all.
3407        """
3408        rawOrders = self.RequestPendingOrders()
3409        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3410        lenOrders = len(allOrdersIDs)
3411
3412        rawStopOrders = self.RequestStopOrders()
3413        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3414        lenSOrders = len(allStopOrdersIDs)
3415
3416        if lenOrders > 0 or lenSOrders > 0:
3417            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3418
3419            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3420
3421        else:
3422            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3424    def CloseAll(self, *args) -> None:
3425        """
3426        Close all available (not blocked) opened trades and orders.
3427
3428        Also, you can select one or more keywords case-insensitive:
3429        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3430
3431        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3432        """
3433        overview = self.Overview(show=False)  # get all open trades info
3434
3435        if len(args) == 0:
3436            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3437            self.CloseAllOrders()  # close all pending and stop orders
3438
3439            for iType in TKS_INSTRUMENTS:
3440                if iType != "Currencies":
3441                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3442
3443        else:
3444            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3445            lowerArgs = [x.lower() for x in args]
3446
3447            if "orders" in lowerArgs:
3448                self.CloseAllOrders()  # close all pending and stop orders
3449
3450            for iType in TKS_INSTRUMENTS:
3451                if iType.lower() in lowerArgs and iType != "Currencies":
3452                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3454    @staticmethod
3455    def ParseOrderParameters(operation, **inputParameters):
3456        """
3457        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3458
3459        :param operation: string "Buy" or "Sell".
3460        :param inputParameters: this is dict of strings that looks like this
3461               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3462               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3463               "prices" key: one or more prices to open limit-orders
3464               Counts of values in lots and prices lists must be equals!
3465        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3466        """
3467        # TODO: update order grid work with api v2
3468        pass
3469        # uLogger.debug("Input parameters: {}".format(inputParameters))
3470        #
3471        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3472        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3473        #     raise Exception("Incorrect value")
3474        #
3475        # if "l" in inputParameters.keys():
3476        #     inputParameters["lots"] = inputParameters.pop("l")
3477        #
3478        # if "p" in inputParameters.keys():
3479        #     inputParameters["prices"] = inputParameters.pop("p")
3480        #
3481        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3482        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3483        #     raise Exception("Incorrect value")
3484        #
3485        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3486        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3487        #
3488        # if len(lots) != len(prices):
3489        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3490        #     raise Exception("Incorrect value")
3491        #
3492        # uLogger.debug("Extracted parameters for orders:")
3493        # uLogger.debug("lots = {}".format(lots))
3494        # uLogger.debug("prices = {}".format(prices))
3495        #
3496        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3497        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3498        # uLogger.debug("Order parameters: {}".format(result))
3499        #
3500        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3502    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3503        """
3504        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3505
3506        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3507        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3508        """
3509        result = False
3510        msg = "Instrument not defined!"
3511
3512        if portfolio is None or not portfolio:
3513            portfolio = self.Overview(show=False)
3514
3515        if self.ticker:
3516            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3517            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3518
3519            for iType in TKS_INSTRUMENTS:
3520                for instrument in portfolio["stat"][iType]:
3521                    if instrument["ticker"] == self.ticker:
3522                        result = True
3523                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3524                        break
3525
3526        elif self.figi:
3527            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3528            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3529
3530            for iType in TKS_INSTRUMENTS:
3531                for instrument in portfolio["stat"][iType]:
3532                    if instrument["figi"] == self.figi:
3533                        result = True
3534                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3535                        break
3536
3537        else:
3538            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3539
3540        uLogger.debug(msg)
3541
3542        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3544    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3545        """
3546        Returns instrument is in the user's portfolio if it presents there.
3547        Instrument must be defined by `ticker` (highly priority) or `figi`.
3548
3549        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3550        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3551        """
3552        result = None
3553        msg = "Instrument not defined!"
3554
3555        if portfolio is None or not portfolio:
3556            portfolio = self.Overview(show=False)
3557
3558        if self.ticker:
3559            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3560            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3561
3562            for iType in TKS_INSTRUMENTS:
3563                for instrument in portfolio["stat"][iType]:
3564                    if instrument["ticker"] == self.ticker:
3565                        result = instrument
3566                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3567                        break
3568
3569        elif self.figi:
3570            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3571            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3572
3573            for iType in TKS_INSTRUMENTS:
3574                for instrument in portfolio["stat"][iType]:
3575                    if instrument["figi"] == self.figi:
3576                        result = instrument
3577                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3578                        break
3579
3580        else:
3581            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3582
3583        uLogger.debug(msg)
3584
3585        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3587    def RequestLimits(self) -> dict:
3588        """
3589        Method for obtaining the available funds for withdrawal for current `accountId`.
3590
3591        See also:
3592        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3593        - `OverviewLimits()` method
3594
3595        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3596                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3597                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3598                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3599        """
3600        if self.accountId is None or not self.accountId:
3601            uLogger.error("Variable `accountId` must be defined for using this method!")
3602            raise Exception("Account ID required")
3603
3604        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3605
3606        self.body = str({"accountId": self.accountId})
3607        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3608        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3609
3610        if self.moreDebug:
3611            uLogger.debug("Records about available funds for withdrawal successfully received")
3612
3613        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3615    def OverviewLimits(self, show: bool = False) -> dict:
3616        """
3617        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3618
3619        See also: `RequestLimits()`.
3620
3621        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3622        :return: dict with raw parsed data from server and some calculated statistics about it.
3623        """
3624        if self.accountId is None or not self.accountId:
3625            uLogger.error("Variable `accountId` must be defined for using this method!")
3626            raise Exception("Account ID required")
3627
3628        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3629
3630        view = {
3631            "rawLimits": rawLimits,
3632            "limits": {  # parsed data for every currency:
3633                "money": {  # this is an array of portfolio currency positions
3634                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3635                },
3636                "blocked": {  # this is an array of blocked currency
3637                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3638                },
3639                "blockedGuarantee": {  # this is locked money under collateral for futures
3640                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3641                },
3642            },
3643        }
3644
3645        # --- Prepare text table with limits in human-readable format:
3646        if show:
3647            info = [
3648                "# Withdrawal limits\n\n",
3649                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3650                "* **Account ID:** [{}]\n".format(self.accountId),
3651            ]
3652
3653            if view["limits"]["money"]:
3654                info.extend([
3655                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3656                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3657                ])
3658
3659            else:
3660                info.append("\nNo withdrawal limits\n")
3661
3662            for curr in view["limits"]["money"].keys():
3663                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3664                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3665                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3666
3667                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3668                    "[{}]".format(curr),
3669                    "{:.2f}".format(view["limits"]["money"][curr]),
3670                    "{:.2f}".format(availableMoney),
3671                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3672                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3673                )
3674
3675                if curr == "rub":
3676                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3677
3678                else:
3679                    info.append(infoStr)
3680
3681            infoText = "".join(info)
3682
3683            uLogger.info(infoText)
3684
3685            if self.withdrawalLimitsFile:
3686                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3687                    fH.write(infoText)
3688
3689                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3690
3691        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3693    def RequestAccounts(self) -> dict:
3694        """
3695        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3696
3697        See also:
3698        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3699        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3700        - `OverviewUserInfo()` method
3701
3702        :return: dict with raw data from server that contains accounts info. Example of dict:
3703                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3704                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3705                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3706                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3707        """
3708        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3709
3710        self.body = str({})
3711        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3712        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3713
3714        if self.moreDebug:
3715            uLogger.debug("Records about available accounts successfully received")
3716
3717        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3719    def RequestUserInfo(self) -> dict:
3720        """
3721        Method for requesting common user's information.
3722
3723        See also:
3724        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3725        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3726        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3727        - `OverviewUserInfo()` method
3728
3729        :return: dict with raw data from server that contains user's information. Example of dict:
3730                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3731                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3732        """
3733        uLogger.debug("Requesting common user's information. Wait, please...")
3734
3735        self.body = str({})
3736        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3737        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3738
3739        if self.moreDebug:
3740            uLogger.debug("Records about current user successfully received")
3741
3742        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3744    def RequestMarginStatus(self, accountId: str = None) -> dict:
3745        """
3746        Method for requesting margin calculation for defined account ID.
3747
3748        See also:
3749        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3750        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3751        - `OverviewUserInfo()` method
3752
3753        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3754        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3755                 Example of responses:
3756                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3757                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3758                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3759                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3760                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3761                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3762        """
3763        if accountId is None or not accountId:
3764            if self.accountId is None or not self.accountId:
3765                uLogger.error("Variable `accountId` must be defined for using this method!")
3766                raise Exception("Account ID required")
3767
3768            else:
3769                accountId = self.accountId  # use `self.accountId` (main ID) by default
3770
3771        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3772
3773        self.body = str({"accountId": accountId})
3774        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3775        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3776
3777        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3778            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3779            rawMargin = {}
3780
3781        else:
3782            if self.moreDebug:
3783                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3784
3785        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3787    def RequestTariffLimits(self) -> dict:
3788        """
3789        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3790
3791        See also:
3792        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3793        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3794        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3795        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3796        - `OverviewUserInfo()` method
3797
3798        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3799                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3800                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3801        """
3802        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3803
3804        self.body = str({})
3805        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3806        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3807
3808        if self.moreDebug:
3809            uLogger.debug("Records with limits of current tariff successfully received")
3810
3811        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3813    def RequestBondCoupons(self, iJSON: dict) -> dict:
3814        """
3815        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3816        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3817        All dates are in UTC timezone.
3818
3819        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3820        Documentation:
3821        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3822        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3823
3824        See also: `ExtendBondsData()`.
3825
3826        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3827                      If raw iJSON is not data of bond then server returns an error [400] with message:
3828                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3829        :return: dictionary with bond payment calendar. Response example
3830                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3831                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3832                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3833                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3834        """
3835        if iJSON["figi"] is None or not iJSON["figi"]:
3836            uLogger.error("FIGI must be defined for using this method!")
3837            raise Exception("FIGI required")
3838
3839        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3840        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3841
3842        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3843            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3844            self.figi,
3845            startDate,
3846            endDate,
3847        ))
3848
3849        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3850        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3851        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3852
3853        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3854            uLogger.warning("Instrument type is not bond!")
3855
3856        else:
3857            if self.moreDebug:
3858                uLogger.debug("Records about bond payment calendar successfully received")
3859
3860        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3862    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3863        """
3864        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3865        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3866        coupon yields, current yields and some statistics etc.
3867
3868        WARNING! This is too long operation if a lot of bonds requested from broker server.
3869
3870        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3871
3872        :param instruments: list of strings with tickers or FIGIs.
3873        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3874                     for further used by data scientists or stock analytics.
3875        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3876                 In XLSX-file and Pandas DataFrame fields mean:
3877                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3878                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3879        """
3880        if instruments is None or not instruments:
3881            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3882            raise Exception("Ticker or FIGI required")
3883
3884        if isinstance(instruments, str):
3885            instruments = [instruments]
3886
3887        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3888
3889        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3890
3891        iCount = len(uniqueInstruments)
3892        tooLong = iCount >= 20
3893        if tooLong:
3894            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3895
3896        bonds = None
3897        for i, self.figi in enumerate(uniqueInstruments):
3898            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3899
3900            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3901                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3902                rawBond = self.SearchByFIGI(requestPrice=True)
3903
3904                # Widen raw data with UTC current time (iData["actualDateTime"]):
3905                actualDate = datetime.now(tzutc())
3906                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3907
3908                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3909                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3910
3911                # Replace some values with human-readable:
3912                iData["nominalCurrency"] = iData["nominal"]["currency"]
3913                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3914                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3915                iData["aciCurrency"] = iData["aciValue"]["currency"]
3916                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3917                iData["issueSize"] = int(iData["issueSize"])
3918                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3919                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3920                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3921                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3922                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3923                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3924                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3925                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3926                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3927                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3928
3929                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3930                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3931                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3932                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3933                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3934                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3935                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3936                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3937                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3938                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3939                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3940
3941                # Widen raw data with calendar data from `rawCalendar` values:
3942                calendarData = []
3943                if "events" in iData["rawCalendar"].keys():
3944                    for item in iData["rawCalendar"]["events"]:
3945                        calendarData.append({
3946                            "couponDate": item["couponDate"],
3947                            "couponNumber": int(item["couponNumber"]),
3948                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3949                            "payCurrency": item["payOneBond"]["currency"],
3950                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3951                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3952                            "couponStartDate": item["couponStartDate"],
3953                            "couponEndDate": item["couponEndDate"],
3954                            "couponPeriod": item["couponPeriod"],
3955                        })
3956
3957                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3958                    if "maturityDate" not in iData.keys():
3959                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3960
3961                # Widen raw data with Coupon Rate.
3962                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3963                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3964                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3965                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3966
3967                # Widen raw data with Yield to Maturity (YTM) on current date.
3968                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3969                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3970                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3971                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3972                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3973                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3974
3975                iData["calendar"] = calendarData  # adds calendar at the end
3976
3977                # Remove not used data:
3978                iData.pop("uid")
3979                iData.pop("positionUid")
3980                iData.pop("currentPrice")
3981                iData.pop("rawCalendar")
3982
3983                colNames = list(iData.keys())
3984                if bonds is None:
3985                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3986
3987                else:
3988                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3989
3990            else:
3991                uLogger.warning("Instrument is not a bond!")
3992
3993            processed = round(100 * (i + 1) / iCount, 1)
3994            if tooLong and processed % 5 == 0:
3995                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3996
3997            else:
3998                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3999
4000        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4001
4002        # Saving bonds from Pandas DataFrame to XLSX sheet:
4003        if xlsx and self.bondsXLSXFile:
4004            with pd.ExcelWriter(
4005                    path=self.bondsXLSXFile,
4006                    date_format=TKS_DATE_FORMAT,
4007                    datetime_format=TKS_DATE_TIME_FORMAT,
4008                    mode="w",
4009            ) as writer:
4010                bonds.to_excel(
4011                    writer,
4012                    sheet_name="Extended bonds data",
4013                    index=True,
4014                    encoding="UTF-8",
4015                    freeze_panes=(1, 1),
4016                )  # saving as XLSX-file with freeze first row and column as headers
4017
4018            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4019
4020        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4022    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4023        """
4024        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4025
4026        WARNING! This is too long operation if a lot of bonds requested from broker server.
4027
4028        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4029
4030        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4031                        extended information about bonds: main info, current prices, bond payment calendar,
4032                        coupon yields, current yields and some statistics etc.
4033                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4034        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4035                     for further used by data scientists or stock analytics.
4036        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4037        """
4038        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4039            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4040
4041        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4042
4043        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4044        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4045        calendar = None
4046        for bond in extBonds.iterrows():
4047            for item in bond[1]["calendar"]:
4048                cData = {
4049                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4050                    "couponDate": item["couponDate"],
4051                    "figi": bond[1]["figi"],
4052                    "ticker": bond[1]["ticker"],
4053                    "name": bond[1]["name"],
4054                    "couponNumber": item["couponNumber"],
4055                    "payOneBond": item["payOneBond"],
4056                    "payCurrency": item["payCurrency"],
4057                    "couponType": item["couponType"],
4058                    "couponPeriod": item["couponPeriod"],
4059                    "fixDate": item["fixDate"],
4060                    "couponStartDate": item["couponStartDate"],
4061                    "couponEndDate": item["couponEndDate"],
4062                }
4063
4064                if calendar is None:
4065                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4066
4067                else:
4068                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4069
4070        if calendar is not None:
4071            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4072
4073            # Saving calendar from Pandas DataFrame to XLSX sheet:
4074            if xlsx:
4075                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4076
4077                with pd.ExcelWriter(
4078                        path=xlsxCalendarFile,
4079                        date_format=TKS_DATE_FORMAT,
4080                        datetime_format=TKS_DATE_TIME_FORMAT,
4081                        mode="w",
4082                ) as writer:
4083                    humanReadable = calendar.copy(deep=True)
4084                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4085                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4086                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4087                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4088                    humanReadable.columns = colNames  # human-readable column names
4089
4090                    humanReadable.to_excel(
4091                        writer,
4092                        sheet_name="Bond payments calendar",
4093                        index=False,
4094                        encoding="UTF-8",
4095                        freeze_panes=(1, 2),
4096                    )  # saving as XLSX-file with freeze first row and column as headers
4097
4098                    del humanReadable  # release df in memory
4099
4100                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4101
4102        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4104    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4105        """
4106        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4107        Also, creates Markdown file with calendar data, `calendar.md` by default.
4108
4109        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4110
4111        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4112                        extended information about bonds: main info, current prices, bond payment calendar,
4113                        coupon yields, current yields and some statistics etc.
4114                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4115        :param show: if `True` then also printing bonds payment calendar to the console,
4116                     otherwise save to file `calendarFile` only. `False` by default.
4117        :return: multilines text in Markdown format with bonds payment calendar as a table.
4118        """
4119        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4120            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4121
4122        infoText = "# Bond payments calendar\n\n"
4123
4124        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4125
4126        if not (calendar is None or calendar.empty):
4127            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4128
4129            info = [
4130                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4131                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4132            ]
4133
4134            newMonth = False
4135            notOneBond = calendar["figi"].nunique() > 1
4136            for i, bond in enumerate(calendar.iterrows()):
4137                if newMonth and notOneBond:
4138                    info.append(splitLine)
4139
4140                info.append(
4141                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4142                        "  √" if bond[1]["paid"] else "  —",
4143                        bond[1]["couponDate"].split("T")[0],
4144                        bond[1]["figi"],
4145                        bond[1]["ticker"],
4146                        bond[1]["couponNumber"],
4147                        "{} {}".format(
4148                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4149                            bond[1]["payCurrency"],
4150                        ),
4151                        bond[1]["couponType"],
4152                        bond[1]["couponPeriod"],
4153                        bond[1]["fixDate"].split("T")[0],
4154                    )
4155                )
4156
4157                if i < len(calendar.values) - 1:
4158                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4159                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4160                    newMonth = False if curDate.month == nextDate.month else True
4161
4162                else:
4163                    newMonth = False
4164
4165            infoText += "".join(info)
4166
4167            if show:
4168                uLogger.info("{}".format(infoText))
4169
4170            if self.calendarFile is not None:
4171                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4172                    fH.write(infoText)
4173
4174                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4175
4176        else:
4177            infoText += "No data\n"
4178
4179        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4181    def OverviewAccounts(self, show: bool = False) -> dict:
4182        """
4183        Method for parsing and show simple table with all available user accounts.
4184
4185        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4186
4187        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4188        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4189                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4190                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4191                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4192                                                        "closed": "—", "access": "Full access" }, ...}}`
4193        """
4194        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4195
4196        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4197        accounts = {
4198            item["id"]: {
4199                "type": TKS_ACCOUNT_TYPES[item["type"]],
4200                "name": item["name"],
4201                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4202                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4203                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4204                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4205            } for item in rawAccounts["accounts"]
4206        }
4207
4208        # Raw and parsed data with some fields replaced in "stat" section:
4209        view = {
4210            "rawAccounts": rawAccounts,
4211            "stat": accounts,
4212        }
4213
4214        # --- Prepare simple text table with only accounts data in human-readable format:
4215        if show:
4216            info = [
4217                "# User accounts\n\n",
4218                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4219                "| Account ID   | Type                      | Status                    | Name                           |\n",
4220                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4221            ]
4222
4223            for account in view["stat"].keys():
4224                info.extend([
4225                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4226                        account,
4227                        view["stat"][account]["type"],
4228                        view["stat"][account]["status"],
4229                        view["stat"][account]["name"],
4230                    )
4231                ])
4232
4233            infoText = "".join(info)
4234
4235            uLogger.info(infoText)
4236
4237            if self.userAccountsFile:
4238                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4239                    fH.write(infoText)
4240
4241                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4242
4243        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4245    def OverviewUserInfo(self, show: bool = False) -> dict:
4246        """
4247        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4248
4249        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4250
4251        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4252        :return: dict with raw parsed data from server and some calculated statistics about it.
4253        """
4254        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4255        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4256        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4257        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4258        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4259        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4260
4261        # This is dict with parsed common user data:
4262        userInfo = {
4263            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4264            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4265            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4266            "tariff": rawUserInfo["tariff"],
4267        }
4268
4269        # This is an array of dict with parsed margin statuses for every account IDs:
4270        margins = {}
4271        for accountId in accounts.keys():
4272            if rawMargins[accountId]:
4273                margins[accountId] = {
4274                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4275                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4276                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4277                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4278                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4279                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4280                }
4281
4282            else:
4283                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4284
4285        unary = {}  # unary-connection limits
4286        for item in rawTariffLimits["unaryLimits"]:
4287            if item["limitPerMinute"] in unary.keys():
4288                unary[item["limitPerMinute"]].extend(item["methods"])
4289
4290            else:
4291                unary[item["limitPerMinute"]] = item["methods"]
4292
4293        stream = {}  # stream-connection limits
4294        for item in rawTariffLimits["streamLimits"]:
4295            if item["limit"] in stream.keys():
4296                stream[item["limit"]].extend(item["streams"])
4297
4298            else:
4299                stream[item["limit"]] = item["streams"]
4300
4301        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4302        limits = {
4303            "unary": unary,
4304            "stream": stream,
4305        }
4306
4307        # Raw and parsed data as an output result:
4308        view = {
4309            "rawUserInfo": rawUserInfo,
4310            "rawAccounts": rawAccounts,
4311            "rawMargins": rawMargins,
4312            "rawTariffLimits": rawTariffLimits,
4313            "stat": {
4314                "userInfo": userInfo,
4315                "accounts": accounts,
4316                "margins": margins,
4317                "limits": limits,
4318            },
4319        }
4320
4321        # --- Prepare text table with user information in human-readable format:
4322        if show:
4323            info = [
4324                "# Full user information\n\n",
4325                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4326                "## Common information\n\n",
4327                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4328                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4329                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4330                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4331                "\n## User accounts\n\n",
4332            ]
4333
4334            for account in view["stat"]["accounts"].keys():
4335                info.extend([
4336                    "### ID: [{}]\n\n".format(account),
4337                    "| Parameters           | Values                                                       |\n",
4338                    "|----------------------|--------------------------------------------------------------|\n",
4339                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4340                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4341                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4342                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4343                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4344                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4345                ])
4346
4347                if margins[account]:
4348                    info.extend([
4349                        "| Margin status:       | Enabled                                                      |\n",
4350                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4351                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4352                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4353                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4354                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4355                    ])
4356
4357                else:
4358                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4359
4360            info.extend([
4361                "\n## Current user tariff limits\n",
4362                "\nSee also:\n",
4363                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4364                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4365                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4366                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4367                "\n### Unary limits\n",
4368            ])
4369
4370            if unary:
4371                for key, values in sorted(unary.items()):
4372                    info.append("\n* Max requests per minute: {}\n".format(key))
4373
4374                    for value in values:
4375                        info.append("  - {}\n".format(value))
4376
4377            else:
4378                info.append("\nNot available\n")
4379
4380            info.append("\n### Stream limits\n")
4381
4382            if stream:
4383                for key, values in sorted(stream.items()):
4384                    info.append("\n* Max stream connections: {}\n".format(key))
4385
4386                    for value in values:
4387                        info.append("  - {}\n".format(value))
4388
4389            else:
4390                info.append("\nNot available\n")
4391
4392            infoText = "".join(info)
4393
4394            uLogger.info(infoText)
4395
4396            if self.userInfoFile:
4397                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4398                    fH.write(infoText)
4399
4400                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4401
4402        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4405class Args:
4406    """
4407    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4408    """
4409    def __init__(self, **kwargs):
4410        self.__dict__.update(kwargs)
4411
4412    def __getattr__(self, item):
4413        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4409    def __init__(self, **kwargs):
4410        self.__dict__.update(kwargs)
def ParseArgs()
4416def ParseArgs():
4417    """This function get and parse command line keys."""
4418    parser = ArgumentParser()  # command-line string parser
4419
4420    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4421    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4422
4423    # --- options:
4424
4425    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4426    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4427    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4428
4429    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4430    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4431
4432    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4433    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4434
4435    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4436
4437    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4438    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4439    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4440
4441    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4442    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4443
4444    # --- commands:
4445
4446    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4447
4448    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4449    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4450    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4451    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4452    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4453    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4454    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4455    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4456
4457    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4458    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4459    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4460    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4461    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4462
4463    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4464    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4465    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4466    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4467
4468    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4469    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4470    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4471
4472    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4473    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4474    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4475    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4476    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4477    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4478    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4479
4480    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4481    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4482    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4483    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4484    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4485
4486    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4487    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4488    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4489
4490    cmdArgs = parser.parse_args()
4491    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4494def Main(**kwargs):
4495    """
4496    Main function for work with TKSBrokerAPI in the console.
4497
4498    See examples:
4499    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4500    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4501    """
4502    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4503
4504    if args.debug_level:
4505        uLogger.level = 10  # always debug level by default
4506        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4507
4508    exitCode = 0
4509    start = datetime.now(tzutc())
4510    uLogger.debug("=-" * 60)
4511    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4512        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4513        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4514    ))
4515
4516    # trying to calculate full current version:
4517    buildVersion = __version__
4518    try:
4519        v = version("tksbrokerapi")
4520        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4521
4522    except Exception:
4523        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4524
4525    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4526    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4527
4528    try:
4529        if args.version:
4530            print("TKSBrokerAPI {}".format(buildVersion))
4531            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4532
4533        else:
4534            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4535            server = TinkoffBrokerServer(
4536                token=args.token,
4537                accountId=args.account_id,
4538                useCache=not args.no_cache,
4539            )
4540
4541            # --- set some options:
4542
4543            if args.more:
4544                server.moreDebug = True
4545                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4546
4547            if args.ticker:
4548                if args.ticker in server.aliasesKeys:
4549                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4550
4551                else:
4552                    server.ticker = args.ticker
4553
4554            if args.figi:
4555                server.figi = args.figi
4556
4557            if args.depth is not None:
4558                server.depth = args.depth
4559
4560            # --- do one command:
4561
4562            if args.list:
4563                if args.output is not None:
4564                    server.instrumentsFile = args.output
4565
4566                server.ShowInstrumentsInfo(show=True)
4567
4568            elif args.list_xlsx:
4569                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4570
4571            elif args.bonds_xlsx is not None:
4572                if args.output is not None:
4573                    server.bondsXLSXFile = args.output
4574
4575                if len(args.bonds_xlsx) == 0:
4576                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4577
4578                else:
4579                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4580
4581            elif args.search:
4582                if args.output is not None:
4583                    server.searchResultsFile = args.output
4584
4585                server.SearchInstruments(pattern=args.search[0], show=True)
4586
4587            elif args.info:
4588                if not (args.ticker or args.figi):
4589                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4590                    raise Exception("Ticker or FIGI required")
4591
4592                if args.output is not None:
4593                    server.infoFile = args.output
4594
4595                if args.ticker:
4596                    server.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4597
4598                else:
4599                    server.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4600
4601            elif args.calendar is not None:
4602                if args.output is not None:
4603                    server.calendarFile = args.output
4604
4605                if len(args.calendar) == 0:
4606                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4607
4608                else:
4609                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4610
4611                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4612
4613            elif args.price:
4614                if not (args.ticker or args.figi):
4615                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4616                    raise Exception("Ticker or FIGI required")
4617
4618                server.GetCurrentPrices(show=True)
4619
4620            elif args.prices is not None:
4621                if args.output is not None:
4622                    server.pricesFile = args.output
4623
4624                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4625
4626            elif args.overview:
4627                if args.output is not None:
4628                    server.overviewFile = args.output
4629
4630                server.Overview(show=True, details="full")
4631
4632            elif args.overview_digest:
4633                if args.output is not None:
4634                    server.overviewDigestFile = args.output
4635
4636                server.Overview(show=True, details="digest")
4637
4638            elif args.overview_positions:
4639                if args.output is not None:
4640                    server.overviewPositionsFile = args.output
4641
4642                server.Overview(show=True, details="positions")
4643
4644            elif args.overview_orders:
4645                if args.output is not None:
4646                    server.overviewOrdersFile = args.output
4647
4648                server.Overview(show=True, details="orders")
4649
4650            elif args.overview_analytics:
4651                if args.output is not None:
4652                    server.overviewAnalyticsFile = args.output
4653
4654                server.Overview(show=True, details="analytics")
4655
4656            elif args.deals is not None:
4657                if args.output is not None:
4658                    server.reportFile = args.output
4659
4660                if 0 <= len(args.deals) < 3:
4661                    server.Deals(
4662                        start=args.deals[0] if len(args.deals) >= 1 else None,
4663                        end=args.deals[1] if len(args.deals) == 2 else None,
4664                        show=True,  # Always show deals report in console
4665                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4666                    )
4667
4668                else:
4669                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4670                    raise Exception("Incorrect value")
4671
4672            elif args.history is not None:
4673                if args.output is not None:
4674                    server.historyFile = args.output
4675
4676                if 0 <= len(args.history) < 3:
4677                    dataReceived = server.History(
4678                        start=args.history[0] if len(args.history) >= 1 else None,
4679                        end=args.history[1] if len(args.history) == 2 else None,
4680                        interval="hour" if args.interval is None or not args.interval else args.interval,
4681                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4682                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4683                        show=True,  # shows all downloaded candles in console
4684                    )
4685
4686                    if args.render_chart is not None and dataReceived is not None:
4687                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4688
4689                        server.ShowHistoryChart(
4690                            candles=dataReceived,
4691                            interact=iChart,
4692                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4693                        )
4694
4695                else:
4696                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4697                    raise Exception("Incorrect value")
4698
4699            elif args.load_history is not None:
4700                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4701
4702                if args.render_chart is not None and histData is not None:
4703                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4704                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4705
4706                    server.ShowHistoryChart(
4707                        candles=histData,
4708                        interact=iChart,
4709                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4710                    )
4711
4712            elif args.trade is not None:
4713                if 1 <= len(args.trade) <= 5:
4714                    server.Trade(
4715                        operation=args.trade[0],
4716                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4717                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4718                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4719                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4720                    )
4721
4722                else:
4723                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4724
4725            elif args.buy is not None:
4726                if 0 <= len(args.buy) <= 4:
4727                    server.Buy(
4728                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4729                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4730                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4731                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4732                    )
4733
4734                else:
4735                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4736
4737            elif args.sell is not None:
4738                if 0 <= len(args.sell) <= 4:
4739                    server.Sell(
4740                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4741                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4742                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4743                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4744                    )
4745
4746                else:
4747                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4748
4749            elif args.order:
4750                if 4 <= len(args.order) <= 7:
4751                    server.Order(
4752                        operation=args.order[0],
4753                        orderType=args.order[1],
4754                        lots=int(args.order[2]),
4755                        targetPrice=float(args.order[3]),
4756                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4757                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4758                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4759                    )
4760
4761                else:
4762                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4763
4764            elif args.buy_limit:
4765                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4766
4767            elif args.sell_limit:
4768                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4769
4770            elif args.buy_stop:
4771                if 2 <= len(args.buy_stop) <= 7:
4772                    server.BuyStop(
4773                        lots=int(args.buy_stop[0]),
4774                        targetPrice=float(args.buy_stop[1]),
4775                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4776                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4777                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4778                    )
4779
4780                else:
4781                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4782
4783            elif args.sell_stop:
4784                if 2 <= len(args.sell_stop) <= 7:
4785                    server.SellStop(
4786                        lots=int(args.sell_stop[0]),
4787                        targetPrice=float(args.sell_stop[1]),
4788                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4789                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4790                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4791                    )
4792
4793                else:
4794                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4795
4796            # elif args.buy_order_grid is not None:
4797            #     # update order grid work with api v2
4798            #     if len(args.buy_order_grid) == 2:
4799            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4800            #
4801            #         for order in orderParams:
4802            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4803            #
4804            #     else:
4805            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4806            #
4807            # elif args.sell_order_grid is not None:
4808            #     # update order grid work with api v2
4809            #     if len(args.sell_order_grid) >= 2:
4810            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4811            #
4812            #         for order in orderParams:
4813            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4814            #
4815            #     else:
4816            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4817
4818            elif args.close_order is not None:
4819                server.CloseOrders(args.close_order)  # close only one order
4820
4821            elif args.close_orders is not None:
4822                server.CloseOrders(args.close_orders)  # close list of orders
4823
4824            elif args.close_trade:
4825                if not (args.ticker or args.figi):
4826                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4827                    raise Exception("Ticker or FIGI required")
4828
4829                if args.ticker:
4830                    server.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4831
4832                else:
4833                    server.CloseTrades([args.figi])  # close only one trade by FIGI
4834
4835            elif args.close_trades is not None:
4836                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4837
4838            elif args.close_all is not None:
4839                server.CloseAll(*args.close_all)
4840
4841            elif args.limits:
4842                if args.output is not None:
4843                    server.withdrawalLimitsFile = args.output
4844
4845                server.OverviewLimits(show=True)
4846
4847            elif args.user_info:
4848                if args.output is not None:
4849                    server.userInfoFile = args.output
4850
4851                server.OverviewUserInfo(show=True)
4852
4853            elif args.account:
4854                if args.output is not None:
4855                    server.userAccountsFile = args.output
4856
4857                server.OverviewAccounts(show=True)
4858
4859            else:
4860                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4861                raise Exception("There is no command to execute")
4862
4863    except Exception:
4864        trace = tb.format_exc()
4865        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4866            if e in trace:
4867                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4868                break
4869
4870        uLogger.debug(trace)
4871        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4872        exitCode = 255  # an error occurred, must be open a ticket for this issue
4873
4874    finally:
4875        finish = datetime.now(tzutc())
4876
4877        if exitCode == 0:
4878            if args.more:
4879                uLogger.debug("All operations were finished success (summary code is 0).")
4880
4881        else:
4882            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4883                os.path.abspath(uLog.defaultLogFile), exitCode,
4884            ))
4885
4886        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4887        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4888            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4889            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4890        ))
4891        uLogger.debug("=-" * 60)
4892
4893        if not kwargs:
4894            sys.exit(exitCode)
4895
4896        else:
4897            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: